diff --git a/riivivetube.py b/riivivetube.py new file mode 100644 index 0000000..5fe155e --- /dev/null +++ b/riivivetube.py @@ -0,0 +1,297 @@ +# work in progress +# videos and more things are not working +# search and trending is working +# swf files from yt2009wii + + +from flask import Flask, send_from_directory, send_file, request, Response, jsonify, stream_with_context +import os +import json +import re +import requests +import xml.etree.ElementTree as ET +from datetime import datetime +import subprocess +import time +import yt_dlp +import youtubei +from concurrent.futures import ThreadPoolExecutor, as_completed +import threading + +app = Flask(__name__) +#stream_cache = {} +#CACHE_DURATION = 300 +executor = ThreadPoolExecutor(max_workers=10) +CATEGORIES = ["trending", "music", "gaming", "sports", "news"] +DL_FOLDER = "./dl" + +if not os.path.exists(DL_FOLDER): + os.makedirs(DL_FOLDER) + +def get_first_video_id_from_route(category): + try: + url = f"http://127.0.0.1:5005/{category}" + response = requests.get(url) + if response.status_code != 200: + print(f"[{category}] HTTP ERROR : HTTP {response.status_code}") + return None + ns = { + 'yt': 'http://www.youtube.com/xml/schemas/2015' + } + + root = ET.fromstring(response.content) + entry = root.find('entry') + if entry is None: + print(f"[{category}] not found") + return None + videoid_el = entry.find('.//yt:videoid', ns) + if videoid_el is None: + print(f"[{category}] not found") + return None + return videoid_el.text.strip() + except Exception as e: + print(f"[{category}] xml parsing error: {e}") + return None + +def download_thumbnail(video_id, category): + url = f"http://i.ytimg.com/vi/{video_id}/hqdefault.jpg" + try: + response = requests.get(url, stream=True) + if response.status_code == 200: + os.makedirs(DL_FOLDER, exist_ok=True) + filepath = os.path.join(DL_FOLDER, f"{category}.jpg") + with open(filepath, 'wb') as f: + for chunk in response.iter_content(1024): + f.write(chunk) + print(f"[{category}] Thumbnail saved: {filepath}") + else: + print(f"[{category}] HTTP ERROR: HTTP {response.status_code}") + except Exception as e: + print(f"[{category}] Error: {e}") + +def thumbnail_scheduler(): + while True: + for category in CATEGORIES: + video_id = get_first_video_id_from_route(category) + if video_id: + download_thumbnail(video_id, category) + else: + print(f"[{category}] video id missing") + time.sleep(600) + +class GetVideoInfo: + def build(self, videoId): + streamUrl = f"https://www.googleapis.com/youtubei/v1/player?videoId={videoId}" + headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + 'Cookie': 'cookie', + 'X-Goog-Visitor-Id': "id", + 'X-Youtube-Bootstrap-Logged-In': 'false' + } + payload = { + "context": { + "client": { + "hl": "en", + "gl": "US", + "clientName": "WEB", + "clientVersion": "2.20231221" + } + }, + "videoId": videoId, + "params": "" + } + response = requests.post(streamUrl, json=payload, headers=headers) + if response.status_code != 200: + return f"Error retrieving video info: {response.status_code}", response.status_code + + try: + json_data = response.json() + # print(json_data) + title = json_data['videoDetails']['title'] + length_seconds = json_data['videoDetails']['lengthSeconds'] + author = json_data['videoDetails']['author'] + except KeyError as e: + return f"Missing key: {e}", 400 + + fmtList = "43/854x480/9/0/115" + fmtStreamMap = f"43|" + fmtMap = "43/0/7/0/0" + thumbnailUrl = f"http://i.ytimg.com/vi/{videoId}/mqdefault.jpg" + + response_str = ( + f"status=ok&" + f"length_seconds={length_seconds}&" + f"keywords=a&" + f"vq=None&" + f"muted=0&" + f"avg_rating=5.0&" + f"thumbnailUrl={thumbnailUrl}&" + f"allow_ratings=1&" + f"hl=en&" + f"ftoken=&" + f"allow_embed=1&" + f"fmtMap={fmtMap}&" + f"fmt_url_map={fmtStreamMap}&" + f"token=null&" + f"plid=null&" + f"track_embed=0&" + f"author={author}&" + f"title={title}&" + f"videoId={videoId}&" + f"fmtList={fmtList}&" + f"fmtStreamMap={fmtStreamMap}" + ) + return Response(response_str, content_type='text/plain') + + +@app.route('/get_video_info', methods=['GET']) +def get_video_info(): + video_id = request.args.get('video_id') + if not video_id: + return jsonify({"error": "Missing video_id parameter"}), 400 + + video_info = GetVideoInfo().build(video_id) + return video_info + +@app.route("/wiitv") +def wiitv(): + return send_from_directory(".", "leanbacklite_wii.swf", mimetype='application/x-shockwave-flash') + + +@app.route("/") +def serve_video(filename): + file_path = os.path.join(filename) + if not os.path.exists(file_path): + return "404 Not found", 404 + return send_file(file_path) + +@app.route('/player_204') +def player(): + return "" + +#@app.route('/get_video', methods=['GET']) +#def get_video(): +#work in progress + + +@app.route('/apiplayer-loader') +def loadapi(): + return send_from_directory('.', 'loader.swf', mimetype='application/x-shockwave-flash') + +@app.route('/videoplayback') +def playback(): + return send_from_directory('.', 'apiplayer.swf', mimetype='application/x-shockwave-flash') + +@app.route('/complete/search') +def completesearch(): + return send_file('search.js') + +class Invidious: + def generateXML(self, json_data): + xml_string = '' + xml_string += '' + xml_string += 'Videos' + xml_string += 'ReviveMiihttp://revivemii.xyz' + xml_string += 'RiiviveTube' + xml_string += f'{len(json_data)}' + xml_string += '1' + xml_string += '20' + + for item in json_data: + xml_string += '' + xml_string += 'http://127.0.0.1/api/videos/' + self.escape_xml(item["videoId"]) + '' + xml_string += '' + self.escape_xml(item.get("publishedText", "")) + '' + xml_string += '' + self.escape_xml(item.get("title", "")) + '' + xml_string += '' + xml_string += '' + self.escape_xml(item.get("author", "")) + '' + xml_string += 'http://127.0.0.1/api/channels/' + self.escape_xml(item.get("authorId", "")) + '' + xml_string += '' + xml_string += '' + xml_string += '' + xml_string += '' + self.escape_xml(item["videoId"]) + '' + xml_string += '' + self.escape_xml(item.get("author", "")) + '' + xml_string += '' + xml_string += '' + xml_string += '' + + xml_string += '' + return xml_string + + def search(self, query): + results = youtubei.innertube_search(query) + return Response(self.generateXML(results), mimetype='text/atom+xml') + + def trends(self, type_param=None): + results = youtubei.innertube_trending(type_param) + return Response(self.generateXML(results), mimetype='text/atom+xml') + + def music(self, type_param=None): + return self.search("music") + + def gaming(self, type_param=None): + return self.search("gaming") + + def sports(self, type_param=None): + return self.search("sports") + + def news(self, type_param=None): + return self.search("news") + + @staticmethod + def escape_xml(s): + if s is None: + return '' + return s.replace('&', '&').replace('<', '<').replace('>', '>')\ + .replace('"', '"').replace("'", ''') + +inv = Invidious() + +@app.route('/feeds/api/videos') +def api_videos(): + query = request.args.get('q') + if not query: + return jsonify({"error": "Missing 'q' parameter"}), 400 + try: + return inv.search(query) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/trending') +def trending(): + try: + return inv.trends() + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/music') +def trending_music(): + try: + return inv.music() + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/gaming') +def trending_gaming(): + try: + return inv.gaming() + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/sports') +def trending_sports(): + try: + return inv.sports() + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/news') +def trending_news(): + try: + return inv.news() + except Exception as e: + return jsonify({"error": str(e)}), 500 + +if __name__ == "__main__": + threading.Thread(target=thumbnail_scheduler, daemon=True).start() + app.run(host="0.0.0.0", port=5005, debug=True)