mirror of
https://github.com/spaam/svtplay-dl.git
synced 2024-11-27 21:54:17 +01:00
248 lines
19 KiB
Python
248 lines
19 KiB
Python
# ex:ts=4:sw=4:sts=4:et
|
|
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
|
|
import json
|
|
import logging
|
|
import re
|
|
import time
|
|
from urllib.parse import urlparse
|
|
|
|
from svtplay_dl.error import ServiceError
|
|
from svtplay_dl.fetcher.dash import dashparse
|
|
from svtplay_dl.fetcher.hls import hlsparse
|
|
from svtplay_dl.service import OpenGraphThumbMixin
|
|
from svtplay_dl.service import Service
|
|
from svtplay_dl.utils.http import download_thumbnails
|
|
|
|
|
|
class Tv4play(Service, OpenGraphThumbMixin):
|
|
supported_domains = ["tv4play.se"]
|
|
|
|
def get(self):
|
|
token = self._login()
|
|
if token is None:
|
|
yield ServiceError("You need a token to access the website. see https://svtplay-dl.se/tv4play/")
|
|
return
|
|
|
|
match = self._getjson(self.get_urldata())
|
|
if not match:
|
|
yield ServiceError("Can't find json data")
|
|
return
|
|
jansson = json.loads(match.group(1))
|
|
|
|
if "params" not in jansson["query"]:
|
|
yield ServiceError("Cant find video id for the video")
|
|
return
|
|
|
|
key_check = None
|
|
for key in jansson["props"]["apolloStateFromServer"]["ROOT_QUERY"].keys():
|
|
if key.startswith("media"):
|
|
key_check = key
|
|
what = jansson["props"]["apolloStateFromServer"]["ROOT_QUERY"][key_check]["__ref"]
|
|
|
|
if what.startswith("Series:"):
|
|
yield ServiceError("Use the video page not the series page")
|
|
return
|
|
else:
|
|
vid = jansson["props"]["apolloStateFromServer"][what]["id"]
|
|
|
|
url = f"https://playback2.a2d.tv/play/{vid}?service=tv4play&device=browser&protocol=hls%2Cdash&drm=widevine&browser=GoogleChrome&capabilities=live-drm-adstitch-2%2Cyospace3"
|
|
res = self.http.request("get", url, headers={"Authorization": f"Bearer {token}"})
|
|
if res.status_code > 400:
|
|
yield ServiceError("Can't play this video because you don't have access to it")
|
|
return
|
|
jansson = res.json()
|
|
|
|
item = jansson["metadata"]
|
|
if item["isDrmProtected"]:
|
|
yield ServiceError("We can't download DRM protected content from this site.")
|
|
return
|
|
|
|
if item["isLive"]:
|
|
self.config.set("live", True)
|
|
if item["seasonNumber"] > 0:
|
|
self.output["season"] = item["seasonNumber"]
|
|
if item["episodeNumber"] and item["episodeNumber"] > 0:
|
|
self.output["episode"] = item["episodeNumber"]
|
|
self.output["title"] = item["seriesTitle"]
|
|
self.output["episodename"] = item["title"]
|
|
self.output["id"] = str(vid)
|
|
self.output["episodethumbnailurl"] = item["image"]
|
|
|
|
if vid is None:
|
|
yield ServiceError("Cant find video id for the video")
|
|
return
|
|
|
|
if jansson["playbackItem"]["type"] == "hls":
|
|
yield from hlsparse(
|
|
self.config,
|
|
self.http.request("get", jansson["playbackItem"]["manifestUrl"]),
|
|
jansson["playbackItem"]["manifestUrl"],
|
|
output=self.output,
|
|
httpobject=self.http,
|
|
)
|
|
yield from dashparse(
|
|
self.config,
|
|
self.http.request("get", jansson["playbackItem"]["manifestUrl"].replace(".m3u8", ".mpd")),
|
|
jansson["playbackItem"]["manifestUrl"].replace(".m3u8", ".mpd"),
|
|
output=self.output,
|
|
httpobject=self.http,
|
|
)
|
|
|
|
def _getjson(self, data):
|
|
match = re.search(r"application\/json\">(.*\})<\/script>", data)
|
|
return match
|
|
|
|
def _login(self):
|
|
if self.config.get("token") is None:
|
|
return None
|
|
res = self.http.request(
|
|
"post",
|
|
"https://avod-auth-alb.a2d.tv/oauth/refresh",
|
|
json={"client_id": "tv4-web", "refresh_token": self.config.get("token")},
|
|
)
|
|
if res.status_code > 400:
|
|
return None
|
|
return res.json()["access_token"]
|
|
|
|
def find_all_episodes(self, config):
|
|
episodes = []
|
|
items = []
|
|
|
|
parse = urlparse(self.url)
|
|
if parse.path.startswith("/klipp"):
|
|
logging.warning("-A on clips is not supported.")
|
|
return episodes
|
|
if parse.path.startswith("/video"):
|
|
logging.warning("Use program page instead of the video one.")
|
|
return episodes
|
|
|
|
token = self._login()
|
|
if token is None:
|
|
logging.error("You need a token to access the website. see https://svtplay-dl.se/tv4play/")
|
|
return episodes
|
|
|
|
showid, jansson, kind = self._get_seriesid(self.get_urldata(), dict())
|
|
if showid is None:
|
|
logging.error("Cant find any videos")
|
|
return episodes
|
|
if showid is False:
|
|
logging.error("Can't play this video because you don't have access to it")
|
|
return episodes
|
|
if kind == "Movie":
|
|
return [f"https://www.tv4play.se/video/{showid}"]
|
|
jansson = self._graphdetails(token, showid)
|
|
for season in jansson["data"]["media"]["allSeasonLinks"]:
|
|
graph_list = self._graphql(season["seasonId"])
|
|
for i in graph_list:
|
|
if i not in items:
|
|
items.append(i)
|
|
|
|
for item in items:
|
|
episodes.append(f"https://www.tv4play.se/video/{item}")
|
|
|
|
if not episodes:
|
|
logging.warning("Can't find any videos")
|
|
else:
|
|
if not self.config.get("reverse_list"):
|
|
episodes = episodes[::-1]
|
|
if config.get("all_last") > 0:
|
|
return episodes[: config.get("all_last")]
|
|
return episodes
|
|
|
|
def _get_seriesid(self, data, jansson):
|
|
match = self._getjson(data)
|
|
if not match:
|
|
return None, jansson, None
|
|
jansson = json.loads(match.group(1))
|
|
if "params" not in jansson["query"]:
|
|
return None, jansson, None
|
|
showid = jansson["query"]["params"][0]
|
|
key_check = None
|
|
for key in jansson["props"]["apolloStateFromServer"]["ROOT_QUERY"].keys():
|
|
if key.startswith("media"):
|
|
key_check = key
|
|
what = jansson["props"]["apolloStateFromServer"]["ROOT_QUERY"][key_check]["__ref"]
|
|
|
|
if what.startswith("Episode"):
|
|
if "series" not in jansson["props"]["apolloStateFromServer"][what]:
|
|
return False, jansson, what[: what.index(":")]
|
|
series = jansson["props"]["apolloStateFromServer"][what]["series"]["__ref"].replace("Series:", "")
|
|
res = self.http.request("get", f"https://www.tv4play.se/program/{series}/")
|
|
showid, jansson = self._get_seriesid(res.text, jansson)
|
|
return showid, jansson, what[: what.index(":")]
|
|
|
|
def _graphdetails(self, token, show):
|
|
data = {
|
|
"operationName": "ContentDetailsPage",
|
|
"query": "query ContentDetailsPage($programId: ID!, $recommendationsInput: MediaRecommendationsInput!, $seriesSeasonInput: SeriesSeasonInput!) {\n media(id: $programId) {\n __typename\n ... on Movie {\n __typename\n id\n title\n genres\n slug\n productionYear\n progress {\n __typename\n percent\n position\n }\n productionCountries {\n __typename\n countryCode\n name\n }\n playableFrom {\n __typename\n isoString\n humanDateTime\n }\n playableUntil {\n __typename\n isoString\n humanDateTime\n readableDistance(type: DAYS_LEFT)\n }\n video {\n __typename\n ...VideoFields\n }\n parentalRating {\n __typename\n ...ParentalRatingFields\n }\n credits {\n __typename\n ...MovieCreditsFields\n }\n label {\n __typename\n ...LabelFields\n }\n images {\n __typename\n main16x7 {\n __typename\n ...ImageFieldsLight\n }\n main16x9 {\n __typename\n ...ImageFieldsFull\n }\n poster2x3 {\n __typename\n ...ImageFieldsLight\n }\n logo {\n __typename\n ...ImageFieldsLight\n }\n }\n synopsis {\n __typename\n brief\n long\n medium\n short\n }\n trailers {\n __typename\n mp4\n webm\n }\n recommendations(input: $recommendationsInput) {\n __typename\n pageInfo {\n __typename\n ...PageInfoFields\n }\n items {\n __typename\n ...RecommendedSeriesMediaItem\n ...RecommendedMovieMediaItem\n }\n }\n hasPanels\n isPollFeatureEnabled\n humanCallToAction\n upsell {\n __typename\n tierId\n }\n }\n ... on Series {\n __typename\n id\n title\n numberOfAvailableSeasons\n genres\n category\n slug\n hasPanels\n isPollFeatureEnabled\n upsell {\n __typename\n tierId\n }\n cdpPageOverride {\n __typename\n id\n }\n upcomingEpisode {\n __typename\n ...UpcomingEpisodeFields\n }\n trailers {\n __typename\n mp4\n webm\n }\n parentalRating {\n __typename\n ...ParentalRatingFields\n }\n credits {\n __typename\n ...SeriesCreditsFields\n }\n label {\n __typename\n ...LabelFields\n }\n images {\n __typename\n main16x7 {\n __typename\n ...ImageFieldsLight\n }\n main16x9 {\n __typename\n ...ImageFieldsFull\n }\n poster2x3 {\n __typename\n ...ImageFieldsLight\n }\n logo {\n __typename\n ...ImageFieldsLight\n }\n }\n synopsis {\n __typename\n brief\n long\n }\n allSeasonLinks {\n __typename\n seasonId\n title\n numberOfEpisodes\n }\n seasonLinks(seriesSeasonInput: $seriesSeasonInput) {\n __typename\n items {\n __typename\n seasonId\n numberOfEpisodes\n }\n }\n suggestedEpisode {\n __typename\n humanCallToAction\n episode {\n __typename\n id\n playableFrom {\n __typename\n isoString\n }\n playableUntil {\n __typename\n isoString\n }\n progress {\n __typename\n percent\n position\n }\n video {\n __typename\n ...VideoFields\n }\n }\n }\n recommendations(input: $recommendationsInput) {\n __typename\n pageInfo {\n __typename\n ...PageInfoFields\n }\n items {\n __typename\n ...RecommendedSeriesMediaItem\n ...RecommendedMovieMediaItem\n }\n }\n }\n ... on SportEvent {\n __typename\n id\n league\n arena\n country\n round\n inStudio\n commentators\n access {\n __typename\n hasAccess\n }\n title\n productionYear\n images {\n __typename\n main16x7 {\n __typename\n ...ImageFieldsFull\n }\n main16x9 {\n __typename\n ...ImageFieldsFull\n }\n poster2x3 {\n __typename\n ...ImageFieldsLight\n }\n }\n trailers {\n __typename\n mp4\n }\n synopsis {\n __typename\n brief\n short\n long\n medium\n }\n playableFrom {\n __typename\n isoString\n humanDateTime\n }\n playableUntil {\n __typename\n isoString\n humanDateTime\n readableDistance(type: DAYS_LEFT)\n }\n liveEventEnd {\n __typename\n isoString\n }\n isLiveContent\n }\n }\n}\nfragment VideoFields on Video {\n __typename\n duration {\n __typename\n readableShort\n seconds\n }\n id\n isDrmProtected\n isLiveContent\n vimondId\n access {\n __typename\n hasAccess\n }\n}\nfragment ParentalRatingFields on ParentalRating {\n __typename\n finland {\n __typename\n ageRestriction\n reason\n }\n sweden {\n __typename\n ageRecommendation\n suitableForChildren\n }\n}\nfragment MovieCreditsFields on MovieCredits {\n __typename\n actors {\n __typename\n characterName\n name\n type\n }\n directors {\n __typename\n name\n type\n }\n}\nfragment LabelFields on Label {\n __typename\n airtime\n announcement\n contentDetailsPage\n recurringBroadcast\n}\nfragment ImageFieldsLight on Image {\n __typename\n source\n}\nfragment ImageFieldsFull on Image {\n __typename\n source\n meta {\n __typename\n muteBgColor {\n __typename\n hex\n }\n }\n}\nfragment PageInfoFields on PageInfo {\n __typename\n hasNextPage\n nextPageOffset\n totalCount\n}\nfragment RecommendedSeriesMediaItem on RecommendedSeries {\n __typename\n series {\n __typename\n id\n title\n images {\n __typename\n cover2x3 {\n __typename\n source\n }\n main16x9 {\n __typename\n source\n meta {\n __typename\n muteBgColor {\n __typename\n hex\n }\n }\n }\n }\n label {\n __typename\n ...LabelFields\n }\n isPollFeatureEnabled\n }\n}\nfragment RecommendedMovieMediaItem on RecommendedMovie {\n __typename\n movie {\n __typename\n id\n title\n images {\n __typename\n cover2x3 {\n __typename\n source\n }\n main16x9 {\n __typename\n source\n meta {\n __typename\n muteBgColor {\n __typename\n hex\n }\n }\n }\n }\n label {\n __typename\n ...LabelFields\n }\n isPollFeatureEnabled\n }\n}\nfragment UpcomingEpisodeFields on UpcomingEpisode {\n __typename\n id\n title\n playableFrom {\n __typename\n humanDateTime\n isoString\n }\n image {\n __typename\n main16x9 {\n __typename\n ...ImageFieldsLight\n }\n }\n}\nfragment SeriesCreditsFields on SeriesCredits {\n __typename\n directors {\n __typename\n name\n type\n }\n hosts {\n __typename\n name\n type\n }\n actors {\n __typename\n characterName\n name\n type\n }\n}",
|
|
"variables": {
|
|
"programId": show,
|
|
"recommendationsInput": {"limit": 10, "offset": 0, "types": ["MOVIE", "SERIES"]},
|
|
"seriesSeasonInput": {"limit": 10, "offset": 0},
|
|
},
|
|
}
|
|
res = self.http.request(
|
|
"post",
|
|
"https://client-gateway.tv4.a2d.tv/graphql",
|
|
headers={"Client-Name": "tv4-web", "Client-Version": "4.0.0", "Content-Type": "application/json", "Authorization": f"Bearer {token}"},
|
|
json=data,
|
|
)
|
|
return res.json()
|
|
|
|
def _graphql(self, show):
|
|
items = []
|
|
nr = 0
|
|
total = 100
|
|
while nr <= total:
|
|
data = {
|
|
"operationName": "SeasonEpisodes",
|
|
"query": "query SeasonEpisodes($seasonId: ID!, $input: SeasonEpisodesInput!) {\n season(id: $seasonId) {\n __typename\n numberOfEpisodes\n episodes(input: $input) {\n __typename\n initialSortOrder\n pageInfo {\n __typename\n ...PageInfoFields\n }\n items {\n __typename\n ...EpisodeFields\n }\n }\n }\n}\nfragment PageInfoFields on PageInfo {\n __typename\n hasNextPage\n nextPageOffset\n totalCount\n}\nfragment EpisodeFields on Episode {\n __typename\n id\n title\n playableFrom {\n __typename\n readableDistance\n timestamp\n isoString\n humanDateTime\n }\n playableUntil {\n __typename\n readableDistance(type: DAYS_LEFT)\n timestamp\n isoString\n humanDateTime\n }\n liveEventEnd {\n __typename\n isoString\n humanDateTime\n timestamp\n }\n progress {\n __typename\n percent\n position\n }\n episodeNumber\n synopsis {\n __typename\n short\n brief\n medium\n }\n seasonId\n series {\n __typename\n id\n title\n images {\n __typename\n main16x9Annotated {\n __typename\n source\n }\n }\n }\n images {\n __typename\n main16x9 {\n __typename\n ...ImageFieldsFull\n }\n }\n video {\n __typename\n ...VideoFields\n }\n isPollFeatureEnabled\n parentalRating {\n __typename\n finland {\n __typename\n ageRestriction\n reason\n containsProductPlacement\n }\n }\n}\nfragment ImageFieldsFull on Image {\n __typename\n source\n meta {\n __typename\n muteBgColor {\n __typename\n hex\n }\n }\n}\nfragment VideoFields on Video {\n __typename\n duration {\n __typename\n readableShort\n seconds\n }\n id\n isDrmProtected\n isLiveContent\n vimondId\n access {\n __typename\n hasAccess\n }\n}",
|
|
"variables": {"input": {"limit": 16, "offset": nr, "sortOrder": "ASC"}, "seasonId": show},
|
|
}
|
|
|
|
res = self.http.request(
|
|
"post",
|
|
"https://client-gateway.tv4.a2d.tv/graphql",
|
|
headers={"Client-Name": "tv4-web", "Client-Version": "4.0.0", "Content-Type": "application/json"},
|
|
json=data,
|
|
)
|
|
janson = res.json()
|
|
total = janson["data"]["season"]["episodes"]["pageInfo"]["totalCount"]
|
|
for mediatype in janson["data"]["season"]["episodes"]["items"]:
|
|
if time.time() < mediatype["playableFrom"]["timestamp"] / 1000:
|
|
continue
|
|
items.append(mediatype["id"])
|
|
nr += 12
|
|
return items
|
|
|
|
def get_thumbnail(self, options):
|
|
download_thumbnails(self.output, options, [(False, self.output["episodethumbnailurl"])])
|
|
|
|
|
|
class Tv4(Service, OpenGraphThumbMixin):
|
|
supported_domains = ["tv4.se"]
|
|
|
|
def get(self):
|
|
match = re.search(r"application\/json\"\>(\{.*\})\<\/script", self.get_urldata())
|
|
if not match:
|
|
yield ServiceError("Can't find video data'")
|
|
return
|
|
janson = json.loads(match.group(1))
|
|
self.output["id"] = janson["query"]["id"]
|
|
self.output["title"] = janson["query"]["slug"]
|
|
if janson["query"]["type"] == "Article":
|
|
vidasset = janson["props"]["pageProps"]["apolloState"][f"Article:{janson['query']['id']}"]["featured"]["__ref"]
|
|
self.output["id"] = janson["props"]["pageProps"]["apolloState"][vidasset]["id"]
|
|
url = f"https://playback2.a2d.tv/play/{self.output['id']}?service=tv4&device=browser&protocol=hls%2Cdash&drm=widevine&capabilities=live-drm-adstitch-2%2Cexpired_assets"
|
|
res = self.http.request("get", url, cookies=self.cookies)
|
|
if res.status_code > 200:
|
|
yield ServiceError("Can't play this because the video is geoblocked.")
|
|
return
|
|
if res.json()["playbackItem"]["type"] == "hls":
|
|
yield from hlsparse(
|
|
self.config,
|
|
self.http.request("get", res.json()["playbackItem"]["manifestUrl"]),
|
|
res.json()["playbackItem"]["manifestUrl"],
|
|
output=self.output,
|
|
)
|