diff --git a/ythdd_inv_tl.py b/ythdd_inv_tl.py index 873e49b..b8c4bfe 100644 --- a/ythdd_inv_tl.py +++ b/ythdd_inv_tl.py @@ -69,6 +69,9 @@ def streams(): def epochToDate(epoch): return strftime('%Y-%m-%dT%H:%M:%SZ', gmtime(epoch)) +def dateToEpoch(date: str): + return datetime.datetime.fromisoformat(date).timestamp() + def trending(): return send(200, [{}]) @@ -105,7 +108,6 @@ def genThumbs(videoId: str): height = x['height'] quality = x['quality'] url = ythdd_globals.config['general']['public_facing_url'] + 'vi/' + videoId + '/' + x['url'] + '.jpg' - #url = '/vi/' + videoId + '/' + x['url'] + '.jpg' result.append({'quality': quality, 'url': url, 'width': width, 'height': height}) return result @@ -113,6 +115,7 @@ def genThumbs(videoId: str): def rebuildFormats(data): result = [{} for x in data] formatStreams = [] + best_bitrate = 0 for x in range(len(data)): @@ -122,6 +125,9 @@ def rebuildFormats(data): except: isVideo = 1 + if not "initRange" in data[x]: # for livestreams? + continue + result[x]['init'] = str(data[x]['initRange']['start']) + "-" + str(data[x]['initRange']['end']) result[x]['index'] = str(data[x]['indexRange']['start']) + "-" + str(data[x]['indexRange']['end']) result[x]['bitrate'] = str(data[x]['averageBitrate']) @@ -157,8 +163,32 @@ def rebuildFormats(data): except: pass - #if data[x]['itag'] <= 80: # won't be triggered for iOS player as it has no progressive streams - # formatStreams.append(result[x]) + # we assume here that a stream with the highest bitrate must be a video stream + if data[x]['averageBitrate'] > data[best_bitrate]['averageBitrate']: + best_bitrate = x + + # makes FreeTube work, unfortunately it's a video-only stream + formatStreams = [ + { + "url": data[best_bitrate]['url'], + "itag": str(data[best_bitrate]['itag']), + "type": data[best_bitrate]['mimeType'], + "quality": data[best_bitrate]['quality'], + "bitrate": str(data[best_bitrate]['averageBitrate']), + "fps": data[best_bitrate]['fps'], + "size": "", # workaround for clipious, which requires ANYTHING to be passed, or else it will throw and error and won't load the video + "resolution": str(invidious_formats.FORMATS[data[best_bitrate]['itag']]['height']) + "p", + "qualityLabel": str(invidious_formats.FORMATS[data[best_bitrate]['itag']]['height']) + "p", + "container": invidious_formats.FORMATS[data[best_bitrate]['itag']]['ext'], + "encoding": invidious_formats.FORMATS[data[best_bitrate]['itag']]['vcodec'] + } + ] + + # not all itags have width and/or height + try: + formatStreams["size"] = str(invidious_formats.FORMATS[data[best_bitrate]['itag']]['width']) + "x" + str(invidious_formats.FORMATS[data[best_bitrate]['itag']]['height']) + except: + pass return result, formatStreams @@ -188,7 +218,7 @@ def videos(data): title = safeTraverse(video_details, ['title'], default=video_id) views = int(safeTraverse(video_details, ['viewCount'], default=0)) length = int(safeTraverse(video_details, ['lengthSeconds'], default=1)) - published = datetime.datetime.fromisoformat(safeTraverse(microformat, ['publishDate'], default="1970-01-02T00:00:00Z")).timestamp() # ISO format to Unix timestamp + published = dateToEpoch(safeTraverse(microformat, ['publishDate'], default="1970-01-02T00:00:00Z")) # ISO format to Unix timestamp published_date = epochToDate(published) premiere_timestamp = safeTraverse(microformat, ['liveBroadcastDetails', 'startTimestamp'], default=0) # let's ignore the nitty gritty for the time being premiere_timestamp = premiere_timestamp if premiere_timestamp else safeTraverse(microformat, ['playabilityStatus', 'liveStreamability', 'liveStreamabilityRenderer', 'offlineSlate', 'liveStreamOfflineSlateRenderer', 'scheduledStartTime'], default=0) @@ -244,8 +274,8 @@ def videos(data): for x in magnitude.keys(): if x in likes_text: likes *= magnitude[x] - description = safeTraverse(microformat, ['description', 'simpleText'], default="\n(ythdd: error ocurred, failed to retrieve description)") - short_description = safeTraverse(wdata, ['ec1', 'videoDetails', 'shortDescription'], default="(ythdd: error occurred, failed to retrieve short description)") + description = safeTraverse(microformat, ['description', 'simpleText'], default="\n(ythdd: failed to retrieve description, perhaps it's empty?)") + short_description = safeTraverse(wdata, ['ec1', 'videoDetails', 'shortDescription'], default="(ythdd: failed to retrieve short description, perhaps it's empty?)") description_html = "
" + description + "
" # sorry, not happening right now, TODO: https://github.com/iv-org/invidious/blob/master/src/invidious/videos/parser.cr#L329 metadata = safeTraverse(video_secondary_renderer, ['metadataRowContainer', 'metadataRowContainerRenderer', 'rows'], default={}) @@ -274,13 +304,14 @@ def videos(data): hls_url = safeTraverse(idata, ['stage1', 'streamingData', 'hlsManifestUrl'], default="") adaptive_formats = safeTraverse(idata, ['stage1', 'streamingData', 'adaptiveFormats'], default=[]) + format_streams = [] adaptive_formats, format_streams = rebuildFormats(adaptive_formats) if live_now: video_type = "livestream" elif premiere_timestamp: video_type = "scheduled" - published = premiere_timestamp if premiere_timestamp else int(time.time()) + published = dateToEpoch(premiere_timestamp) if premiere_timestamp else int(time.time()) else: video_type = "video" @@ -289,11 +320,6 @@ def videos(data): premium = True # TODO: detect paywalled patron-only videos - if not format_streams: - format_streams = [] - # providing format streams breaks Clipious client - #format_streams.append(adaptive_formats[0]) - #format_streams.append(adaptive_formats[1]) #''' response = { @@ -335,7 +361,7 @@ def videos(data): "liveNow": live_now, "isPostLiveDvr": post_live_dvr, "isUpcoming": is_upcoming, - "dashUrl": "/dash/not/implemented/", # not implemented + "dashUrl": ythdd_globals.config['general']['public_facing_url'] + "/dash/not/implemented/", # not implemented "premiereTimestamp": premiere_timestamp, "hlsUrl": hls_url, @@ -418,10 +444,10 @@ def lookup(data): case _: incrementBadRequests() return notImplemented(data) - elif data[0] == 'ggpht': + elif data[0] in ('ggpht', 'vi'): # for some reason the Materialous client - # keeps making requests to these - if data[1] == 'ggpht': + # and FreeTube keep making requests to these + if data[1] in ('ggpht', 'vi'): return redirect('/' + "/".join(data[1:])) return redirect('/' + "/".join(data[0:])) else: @@ -432,7 +458,7 @@ def lookup(data): return stats() elif data[0] == "streams": return streams() - elif data[0] == 'ggpht': + elif data[0] in ('ggpht', 'vi'): return redirect('/' + "/".join(data[0:])) else: incrementBadRequests()