freetv-downloader/main.py

418 lines
15 KiB
Python

# This is a sample Python script.
import base64
import json
import os
import pathlib
import http.cookiejar
import re
import subprocess
import sys
import uuid
from datetime import datetime,timezone
from typing import Any
import requests as requests
import xmltodict as xmltodict
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
from http.cookiejar import Cookie
cdmKeyFile=None
session= requests.Session()
output_format ="mkv"
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
import string
import unicodedata
def get_int(list_len, accept_zero=False):
while True:
try:
num = int(input("Choose "))
if num > list_len + 1 or (num == 0 and not accept_zero):
print("no such option")
continue
break
except ValueError:
print("Invalid input")
continue
return num
INVALID_FILE_CHARS = '/\\?%*:|"<>' # https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
def secure_filename(filename):
# keep only valid ascii chars
output = list(unicodedata.normalize("NFKD", filename))
# special case characters that don't get stripped by the above technique
for pos, char in enumerate(output):
if char == '\u0141':
output[pos] = 'L'
elif char == '\u0142':
output[pos] = 'l'
# remove unallowed characters
output = [c if c not in INVALID_FILE_CHARS else '' for c in output]
return "".join(output).encode("utf-8", "ignore").decode()
def get_data_after_last_comma(s):
# Find the last occurrence of a comma
last_comma_index = s.rfind(',')
# Slice the string from the character after the last comma to the end
data_after_last_comma = s[last_comma_index+1:] if last_comma_index != -1 else ""
return data_after_last_comma.strip()
def getplaylist(vidid,apiddevid,apicorelid,refferer):
playlisturl = 'https://web.freetv.tv/api/products/'+str(vidid)+'/videos/playlist?lang=HEB&maxRating=18&platform=BROWSER&videoType=MOVIE'
plaulist_headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
'api-correlationid': apicorelid,
'api-deviceuid': apiddevid,
'referer': refferer,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
playlist_req = session.get(playlisturl,headers=plaulist_headers)
playlist_req.raise_for_status()
return playlist_req.json()
def parse_mpd(mpd_url):
xml = xmltodict.parse(mpd_url)
mpd = json.loads(json.dumps(xml))
tracks = mpd['MPD']['Period']['AdaptationSet']
for video_tracks in tracks:
if video_tracks['@mimeType'] == 'video/mp4':
wysokosc = video_tracks['@maxHeight']
for t in video_tracks["ContentProtection"]:
if t['@schemeIdUri'].lower() == "urn:mpeg:dash:mp4protection:2011":
try:
kid = t["@cenc:default_KID"]
except:
kid = t["@default_KID"]
return kid, wysokosc
def getkey(licenseurl, apidevid, apicorelationid, referer,manurl):
mpdhead = {
'accept': '*/*',
'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
'referer': 'https://web.freetv.tv/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
licheaders = {
'accept': '*/*',
'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
'origin': 'https://web.freetv.tv/',
'referer': referer,
'api-correlationid': apicorelationid,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
mpdxml = requests.get(manurl, headers=mpdhead).text
# PSSH =0000003b7073736800000000edef8ba979d64acea3c827dcd51d21ed0000001b08011210+kid+2a05415544494f
pssh_enc = bytes.fromhex('0000003b7073736800000000edef8ba979d64acea3c827dcd51d21ed0000001b08011210')
kid = parse_mpd(mpdxml)[0]
kid = str(kid).replace("-", "").lower()
pssh_enc = pssh_enc + bytes.fromhex(kid)
pssh_enc += bytes.fromhex("2a05415544494f")
pssh_base64 = str(base64.b64encode(pssh_enc), "ascii")
pssh = PSSH(pssh_base64)
device = Device.load(cdmKeyFile)
cdm = Cdm.from_device(device)
session_id = cdm.open()
challenge = cdm.get_license_challenge(session_id, pssh)
licence = session.post(licenseurl, data=challenge, headers=licheaders)
licence.raise_for_status()
cdm.parse_license(session_id, licence.content)
fkeys = ""
for key in cdm.get_keys(session_id):
if key.type != 'SIGNING' and str(key.kid.hex).lower() == kid:
fkeys += key.kid.hex + ":" + key.key.hex()
return fkeys
def checkhascdm():
global cdmKeyFile
data_path = os.path.join(os.path.dirname(sys.argv[0]),"data")
if not os.path.exists(data_path):
os.makedirs(data_path, exist_ok=True)
keysdir = os.path.join(data_path,"keys")
if not os.path.exists(keysdir):
os.makedirs(keysdir, exist_ok=True)
keysdir_content = os.listdir(keysdir)
for file in keysdir_content:
if file.endswith('.wvd'):
cdmKeyFile = file
if cdmKeyFile is None:
print("paste wvd file into data/keys")
sys.exit(1)
else:
cdmKeyFile = os.path.join(keysdir,cdmKeyFile)
def refresh_sessionuuid(apicorelid, apifrbid):
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
r =session.get("https://web.freetv.tv/api/subscribers/detail?lang=HEB&platform=BROWSER")
r.raise_for_status()
resp = r.json()
return resp['httpSession']['uid'],resp['httpSession']['till']
def main():
# Use a breakpoint in the code line below to debug your script.
print("DL script for web.free.tv")
checkhascdm()
if not pathlib.Path('cookies.txt').is_file():
print("put cookies txt in the folder where this script is ")
return 1
url =input("Please provide url of movie or series or episode: ")
print("Output formats:")
print("0. mkv")
print("1.mp4")
choice = get_int(1,True)
if choice == 1:
global output_format
output_format = "mp4"
session.cookies = http.cookiejar.MozillaCookieJar('cookies.txt')
session.cookies.load(ignore_discard=True, ignore_expires=True)
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
})
apifrbid=None
for cookie in session.cookies:
if cookie.name == 'uid':
apifrbid=cookie.value
if apifrbid is None:
print("api device id not found in cookies")
return 1
print(apifrbid)
apicorelid= 'client_'+str(uuid.uuid4())
isSessionValid = False
session_uuid = None
if True:
if True:
ret = refresh_sessionuuid(apicorelid, apifrbid)
isSessionValid = True
cookie = Cookie(version=0, name='session_uid', value=ret[0],
port=None, port_specified=False, domain='web.freetv.tv', domain_specified=False,
domain_initial_dot=False,
path='/', path_specified=False, secure=True, expires=ret[1],
discard=False, comment=None, comment_url=None, rest={}, rfc2109=False)
session.cookies.set_cookie(cookie)
else:
isSessionValid = True
if not ',' in url:
print("incorrect url format")
return 1
vid_id =get_data_after_last_comma(url)
metadata = get_metadata(apicorelid, apifrbid, url, vid_id)
print(metadata)
if metadata['type_'] == 'SERIAL' or metadata['type_'] == 'SERIES':
print("TV show detected")
download_show(apicorelid, apifrbid, metadata, url, vid_id)
elif metadata['type_'] == 'EPISODE':
episode_id = get_data_after_last_comma(url)
show_url = remove_after_last_slash(url)
show_id = get_data_after_last_comma(show_url)
seasons = get_seasons(apicorelid,apifrbid,show_url,show_id)
show_metadata = get_metadata(apicorelid, apifrbid,show_url, show_id )
seson_for_epsode = None
for season in seasons:
episodes = get_seson_episodes(apicorelid,apifrbid,season,show_id)
for episode in episodes:
if str(episode['id']) == episode_id:
seson_for_epsode = season
break
download_episode(apicorelid,apifrbid,metadata,show_metadata,seson_for_epsode)
elif metadata['type_'] == 'VOD':
movie_id = get_data_after_last_comma(url)
playlist = getplaylist(movie_id,apifrbid,apicorelid,url)
mainfest_url: str | Any = 'https:' + playlist['sources']['DASH'][0]['src']
licenseurl = playlist['drm']['WIDEVINE']['src']
key = getkey(licenseurl, apifrbid, apicorelid, url, mainfest_url)
title = metadata['title'] + '.'+output_format
title = secure_filename(title)
args = [
"N_m3u8DL-RE.exe",
mainfest_url,
"--log-level",
"ERROR",
"--binary-merge",
"--live-real-time-merge",
"--mp4-real-time-decryption",
"--key",
key,
"-M",
"format="+output_format,
"--del-after-done",
"-ss",
"all",
"-sa",
"best",
"-sv",
"best",
"--save-name",
'temp',
"--save-dir",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "output"),
"-mt",
"TRUE",
"--thread-count",
"12"
]
subprocess.call(args)
output_filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output", "temp."+output_format)
final_filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output", title)
os.rename(output_filename, final_filename)
else:
print(metadata)
session.cookies.save()
return 0
def remove_after_last_slash(s: str) -> str:
"""
This function removes everything from the string after the last slash, including the slash itself.
Parameters:
- s (str): The input string from which to remove the portion after the last slash.
Returns:
- str: The modified string with everything after the last slash removed.
"""
# Find the index of the last slash in the string
last_slash_index = s.rfind('/')
# If a slash is found, return the string up to the slash. Otherwise, return the original string.
if last_slash_index != -1:
return s[:last_slash_index]
else:
return s
def download_show(apicorelid, apifrbid, metadata, url, vid_id):
seasons = get_seasons(apicorelid, apifrbid, url, vid_id)
print(seasons)
print("Choose what to download:\n 0:whole show")
for idx,s in enumerate(seasons):
print(str(idx+1)+"Season "+str(s['number']))
choice = get_int(len(seasons),True)
if choice != 0:
eps = get_seson_episodes(apicorelid, apifrbid,seasons[choice-1], vid_id)
for ep in eps:
download_episode(apicorelid, apifrbid, ep, metadata, seasons[choice-1])
return
for ses in seasons:
eps = get_seson_episodes(apicorelid, apifrbid, ses, vid_id)
print(eps)
for ep in eps:
download_episode(apicorelid, apifrbid, ep, metadata, ses)
def download_episode(apicorelid, apifrbid, ep, metadata, ses):
print(ep)
playlist = getplaylist(ep['id'], apifrbid, apicorelid, ep['webUrl'])
print(playlist)
mainfest_url: str | Any = 'https:' + playlist['sources']['DASH'][0]['src']
licenseurl = playlist['drm']['WIDEVINE']['src']
key = getkey(licenseurl, apifrbid, apicorelid, ep['webUrl'], mainfest_url)
print(key)
title = metadata['title'] + ' S' + str(ses['number']) + 'E' + str(ep['number']) + ' ' + ep['title'] + "."+output_format
title = secure_filename(title)
args = [
"N_m3u8DL-RE.exe",
mainfest_url,
"--log-level",
"ERROR",
"--binary-merge",
"--live-real-time-merge",
"--mp4-real-time-decryption",
"--key",
key,
"-M",
"format="+output_format,
"--del-after-done",
"-ss",
"all",
"-sa",
"best",
"-sv",
"best",
"--save-name",
'temp',
"--save-dir",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "output"),
"-mt",
"TRUE",
"--thread-count",
"12"
]
subprocess.call(args)
output_filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output", "temp."+output_format)
final_filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output", title)
os.rename(output_filename, final_filename)
def get_seson_episodes(apicorelid, apifrbid, ses, show_id):
episodes_ref = ses['webUrl']
episodes_url = 'https://web.freetv.tv/api/products/vods/serials/' + show_id + '/seasons/' + str(
ses['id']) + '/episodes?lang=HEB&maxRating=18&platform=BROWSER'
episodes_head = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
'api-correlationid': apicorelid,
'api-deviceuid': apifrbid,
'referer': episodes_ref,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
episodes_request = session.get(episodes_url, headers=episodes_head)
episodes_request.raise_for_status()
eps = episodes_request.json()
return eps
def get_seasons(apicorelid, apifrbid, url, vid_id):
seasons_url = "https://web.freetv.tv/api/products/vods/serials/" + vid_id + '/seasons?lang=HEB&maxRating=18&platform=BROWSER'
shead = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
'api-correlationid': apicorelid,
'api-deviceuid': apifrbid,
'referer': url,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
req = session.get(seasons_url, headers=shead)
req.raise_for_status()
seasons = req.json()
return seasons
def get_metadata(apicorelid, apifrbid, url, vid_id):
metadat_url = 'https://web.freetv.tv/api/products/vods/' + vid_id + "?lang=HEB&maxRating=18&platform=BROWSER"
metaheaders = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
'api-correlationid': apicorelid,
'api-deviceuid': apifrbid,
'referer': url,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
}
r = session.get(metadat_url, headers=metaheaders)
r.raise_for_status()
metadata = r.json()
return metadata
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
main()
# See PyCharm help at https://www.jetbrains.com/help/pycharm/