11 Commits

Author SHA1 Message Date
stabbedbybrick
454f9b90c7 update 2023-09-30 20:33:05 +02:00
stabbedbybrick
c202aff294 update service 2023-09-30 20:29:14 +02:00
stabbedbybrick
75da6e96d7 update readme 2023-09-30 20:29:03 +02:00
stabbedbybrick
1fafdb2f5d update version 2023-09-30 20:22:02 +02:00
stabbedbybrick
0445b65028 update readme 2023-09-30 20:21:53 +02:00
stabbedbybrick
bce9ff2db2 fix standalone episodes 2023-09-30 20:19:42 +02:00
stabbedbybrick
b431622a59 season num correction 2023-09-30 20:04:10 +02:00
stabbedbybrick
9e910eaf96 update 2023-09-30 19:38:04 +02:00
stabbedbybrick
f805cf80a0 update 2023-09-30 19:37:41 +02:00
stabbedbybrick
ba6ef7385f bbc service 2023-09-30 19:37:11 +02:00
stabbedbybrick
eb59aec1be added bbc 2023-09-30 19:36:52 +02:00
8 changed files with 380 additions and 40 deletions

View File

@@ -15,6 +15,7 @@ Download videos from free streaming services
ROKU: 1080p, DD5.1
CTV: 1080p, DD5.1
ALL4: 1080p, AAC2.0
BBCiP: 1080p, AAC2.0
MY5: 1080p, AAC2.0
UKTV: 1080p, AAC2.0
STV: 1080p, AAC2.0

View File

@@ -1 +1 @@
__version__ = "v0.5.2-beta (20230927)"
__version__ = "v0.5.3-beta (20230930)"

View File

@@ -47,6 +47,7 @@ main_help = f"""
ROKU: 1080p, DD5.1
CTV: 1080p, DD5.1
ALL4: 1080p, AAC2.0
BBCiP: 1080p, AAC2.0
MY5: 1080p, AAC2.0*
UKTV: 1080p, AAC2.0
STV: 1080p, AAC2.0

View File

@@ -31,7 +31,9 @@ def get_service(url: str):
parse = urlparse(url)
netloc = parse.netloc.split(".")
if len(netloc) == 3 and netloc[2] == "uk":
if len(netloc) == 4:
domain = netloc[1]
elif len(netloc) == 3 and netloc[2] == "uk":
domain = netloc[0]
elif len(netloc) == 3:
domain = netloc[1]
@@ -42,7 +44,7 @@ def get_service(url: str):
if any(
re.search(pattern, parse.path)
for pattern in [r"\d+-\d+$", r"s\d+e\d+$", "episode"]
for pattern in [r"\d+-\d+$", r"s\d+e\d+$"]
):
error("Wrong URL format. Use series URL, not episode URL")
sys.exit(1)

View File

@@ -7,28 +7,40 @@ from helpers.utilities import string_cleaning
class Episode:
def __init__(self, **kwargs) -> None:
self.id = kwargs.get("id_")
self.service = kwargs.get("service")
self.title = kwargs.get("title")
self.season = kwargs.get("season")
self.number = kwargs.get("number")
self.name = kwargs.get("name")
self.year = kwargs.get("year")
self.data = kwargs.get("data")
self.subtitle = kwargs.get("subtitle")
self.lic_url = kwargs.get("lic_url")
self.synopsis = kwargs.get("synopsis")
self.description = kwargs.get("description")
def __init__(
self,
id_=None,
service=None,
title=None,
season=None,
number=None,
name=None,
year=None,
data=None,
subtitle=None,
lic_url=None,
synopsis=None,
description=None,
) -> None:
if name is not None:
name = name.strip()
if re.match(r"Episode ?#?\d+", name, re.IGNORECASE):
name = None
elif name.lower() == title.lower():
name = None
self.title = self.title.strip()
if self.name is not None:
self.name = self.name.strip()
if re.match(r"Episode ?#?\d+", self.name, re.IGNORECASE):
self.name = None
elif self.name.lower() == self.title.lower():
self.name = None
self.id = id_
self.service = service
self.title = title
self.season = season
self.number = number
self.name = name
self.year = year
self.data = data
self.subtitle = subtitle
self.lic_url = lic_url
self.synopsis = synopsis
self.description = description
def __str__(self) -> str:
return "{title} S{season:02}E{number:02} {name}".format(
@@ -60,18 +72,31 @@ class Series(SortedKeyList, ABC):
class Movie:
def __init__(self, **kwargs) -> None:
self.id = kwargs.get("id_")
self.service = kwargs.get("service")
self.title = kwargs.get("title")
self.name = kwargs.get("name")
self.year = kwargs.get("year")
self.data = kwargs.get("data")
self.subtitle = kwargs.get("subtitle")
self.lic_url = kwargs.get("lic_url")
self.synopsis = kwargs.get("synopsis")
self.name = self.name.strip()
def __init__(
self,
id_=None,
service=None,
title=None,
name=None,
year=None,
data=None,
subtitle=None,
lic_url=None,
synopsis=None,
) -> None:
if name is not None:
name = name.strip()
self.id = id_
self.service = service
self.title = title
self.name = name
self.year = year
self.data = data
self.subtitle = subtitle
self.lic_url = lic_url
self.synopsis = synopsis
def __str__(self) -> str:
if self.year:
@@ -91,4 +116,4 @@ class Movies(SortedKeyList, ABC):
def __str__(self) -> str:
if not self:
return super().__str__()
return self[0].name + (f" ({self[0].year})" if self[0].year else "")
return self[0].name + (f" ({self[0].year})" if self[0].year else "")

305
services/bbc.py Normal file
View File

@@ -0,0 +1,305 @@
"""
BBC iplayer
Author: stabbedbybrick
Info:
up to 1080p
"""
import subprocess
import re
from collections import Counter
from urllib.parse import urlparse, urlunparse
import click
import requests
from bs4 import BeautifulSoup
from helpers.utilities import (
info,
string_cleaning,
set_save_path,
print_info,
set_filename,
)
from helpers.titles import Episode, Series, Movie, Movies
from helpers.args import Options, get_args
from helpers.config import Config
class BBC(Config):
def __init__(self, config, srvc, **kwargs):
super().__init__(config, srvc, **kwargs)
self.get_options()
def get_data(self, pid: str, slice_id:str) -> dict:
json_data = {
'id': '9fd1636abe711717c2baf00cebb668de',
'variables': {
'id': pid,
'perPage': 200,
'page': 1,
'sliceId': slice_id if slice_id else None,
},
}
response = self.client.post(self.srvc["bbc"]["api"], json=json_data).json()
return response["data"]["programme"]
def create_episode(self, episode):
subtitle = episode["episode"]["subtitle"]
title = subtitle.get("default") or subtitle.get("slice") or ""
season_match = re.search(r"Series (\d+):", subtitle.get("default"))
season = int(season_match.group(1)) if season_match else 0
number_match = re.finditer(r"(\d+)\.|Episode (\d+)|Week (\d+)", title)
number = int(next((m.group(1) or m.group(2) or m.group(3) for m in number_match), 0))
name_match = re.search(r"\d+\. (.+)", subtitle.get("slice") or subtitle.get("default") or "")
name = name_match.group(1) if name_match else ""
return Episode(
id_=episode["episode"]["id"],
service="iP",
title=episode["episode"]["title"]["default"],
season=season,
number=number,
name=name,
description=episode["episode"]["synopsis"].get("small"),
)
def get_series(self, pid: str, slice_id:str) -> Series:
data = self.get_data(pid, slice_id)
seasons = [self.get_data(pid, x["id"]) for x in data["slices"] or [{"id": None}]]
episodes = [
self.create_episode(episode)
for season in seasons
for episode in season["entities"]["results"]
]
return Series(episodes)
def get_movies(self, pid: str, slice_id: str) -> Movies:
data = self.get_data(pid, slice_id)
return Movies(
[
Movie(
id_=data["id"],
service="iP",
title=data["title"]["default"],
year=None, # TODO
name=data["title"]["default"],
synopsis=data["synopsis"].get("small"),
)
]
)
def add_stream(self, soup: object, init: str) -> object:
representation = soup.new_tag("Representation",
id="video=12000000",
bandwidth="8490000",
width="1920",
height="1080",
frameRate="50",
codecs="avc3.640020",
scanType="progressive",
)
template = soup.new_tag("SegmentTemplate",
timescale="5000",
duration="19200",
initialization=f"{init}-$RepresentationID$.dash",
media=f"{init}-$RepresentationID$-$Number$.m4s",
)
representation.append(template)
soup.find("AdaptationSet", {"contentType": "video"}).append(representation)
return soup
def get_playlist(self, pid: str) -> tuple:
resp = self.client.get(
self.srvc["bbc"]["playlist"].format(pid=pid)).json()
vpid = resp["defaultAvailableVersion"]["smpConfig"]["items"][0]["vpid"]
media = self.client.get(
self.srvc["bbc"]["media"].format(vpid=vpid)
).json()
subtitle = None
for item in media["media"]:
if item["kind"] == "video":
videos = item["connection"]
for item in media["media"]:
if item["kind"] == "captions":
captions = item["connection"]
for video in videos:
if video["supplier"] == "mf_bidi" and video["transferFormat"] == "dash": # TODO
manifest = video["href"]
for caption in captions:
if caption["supplier"] == "mf_bidi" or "mf_cloudfront":
subtitle = caption["href"]
soup = BeautifulSoup(requests.get(manifest).content, "xml")
parse = urlparse(manifest)
_path = parse.path.split("/")
_path[-1] = "dash/"
init = _path[-2].replace(".ism", "")
base_url = urlunparse(parse._replace(
scheme="https",
netloc=self.srvc["bbc"]["base"],
path="/".join(_path),
query=""
)
)
soup.select_one("BaseURL").string = base_url
tag = soup.find(id="video=5070000")
if tag:
soup = self.add_stream(soup, init)
with open(self.tmp / "manifest.mpd", "w") as f:
f.write(str(soup.prettify()))
self.soup = soup
return soup, subtitle
def get_mediainfo(self, soup: object, quality: str) -> str:
elements = soup.find_all("Representation")
heights = sorted(
[int(x.attrs["height"]) for x in elements if x.attrs.get("height")],
reverse=True,
)
if quality is not None:
if int(quality) in heights:
return quality
else:
closest_match = min(heights, key=lambda x: abs(int(x) - int(quality)))
return closest_match
return heights[0]
def get_content(self, url: str) -> object:
if self.movie:
with self.console.status("Fetching titles..."):
parse = urlparse(url)
pid = parse.path.split("/")[3]
slice_id = parse.query.split("=")[1] if parse.query else None
content = self.get_movies(pid, slice_id)
title = string_cleaning(str(content))
info(f"{str(content)}\n")
else:
with self.console.status("Fetching titles..."):
parse = urlparse(url)
pid = parse.path.split("/")[3]
slice_id = parse.query.split("=")[1] if parse.query else None
content = self.get_series(pid, slice_id)
counter = 1
for episode in content:
episode.name = episode.get_filename()
if "S00E00" in episode.name:
episode.name = episode.name.replace("E00", f"E{counter:02d}")
counter += 1
title = string_cleaning(str(content))
seasons = Counter(x.season for x in content)
num_seasons = len(seasons)
num_episodes = sum(seasons.values())
info(
f"{str(content)}: {num_seasons} Season(s), {num_episodes} Episode(s)\n"
)
return content, title
def get_options(self) -> None:
opt = Options(self)
content, title = self.get_content(self.url)
if self.episode:
downloads = opt.get_episode(content)
if self.season:
downloads = opt.get_season(content)
if self.complete:
downloads = opt.get_complete(content)
if self.movie:
downloads = opt.get_movie(content)
if self.titles:
opt.list_titles(content)
for download in downloads:
self.download(download, title)
def clean_subtitles(self, subtitle: str, filename: str):
"""
Temporary solution, but seems to work for the most part
"""
with self.console.status("Cleaning up subtitles..."):
soup = BeautifulSoup(requests.get(subtitle).content, "xml")
for tag in soup.find_all():
if tag.name != "p" and tag.name != "br" and tag.name != "span":
tag.unwrap()
for br in soup.find_all("br"):
br.replace_with(" ")
srt = ""
for i, tag in enumerate(soup.find_all("p")):
start = tag["begin"]
end = tag["end"]
text = tag.get_text().strip()
srt += f"{i+1}\n{start.replace('.', ',')} --> {end.replace('.', ',')}\n{text}\n\n"
with open(self.tmp / f"{filename}.srt", "w") as f:
f.write(srt)
self.sub_path = self.tmp / f"{filename}.srt"
def download(self, stream: object, title: str) -> None:
with self.console.status("Getting media info..."):
soup, subtitle = self.get_playlist(stream.id)
res = self.get_mediainfo(soup, self.quality)
self.filename = set_filename(self, stream, res, audio="AAC2.0")
self.save_path = set_save_path(stream, self.config, title)
self.manifest = self.tmp / "manifest.mpd"
self.key_file = None # not encrypted
self.sub_path = None
if subtitle is not None:
self.clean_subtitles(subtitle, self.filename)
if self.info:
print_info(self, stream, keys=None)
info(f"{stream.name}")
click.echo("")
args, file_path = get_args(self, res)
if not file_path.exists():
try:
subprocess.run(args, check=True)
except:
raise ValueError("Download failed or was interrupted")
else:
info(f"{self.filename} already exist. Skipping download\n")
self.sub_path.unlink() if self.sub_path else None
pass

View File

@@ -59,8 +59,8 @@ class CHANNEL5(Config):
id_=None,
service="MY5",
title=episode.get("sh_title"),
season=episode.get("sea_num") or 0,
number=episode.get("ep_num") or 0,
season=int(episode.get("sea_num")) or 0,
number=int(episode.get("ep_num")) or 0,
name=episode.get("title"),
year=None,
data=episode.get("id"),

View File

@@ -36,4 +36,10 @@ roku:
pluto:
api: "https://service-vod.clusters.pluto.tv/v4/vod"
lic: "https://service-concierge.clusters.pluto.tv/v1/wv/alt"
lic: "https://service-concierge.clusters.pluto.tv/v1/wv/alt"
bbc:
api: "https://graph.ibl.api.bbc.co.uk/"
media: "https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/iptv-all/vpid/{vpid}/"
playlist: "https://www.bbc.co.uk/programmes/{pid}/playlist.json"
base: "b4-thdow-bbc.live.bidi.net.uk"