mirror of
https://github.com/ReviveMii/riivivetube
synced 2025-09-02 19:41:07 +02:00
Create riivivetube.py
This commit is contained in:
parent
7b0662e3ab
commit
3d582ba857
297
riivivetube.py
Normal file
297
riivivetube.py
Normal file
@ -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}] <entry> not found")
|
||||
return None
|
||||
videoid_el = entry.find('.//yt:videoid', ns)
|
||||
if videoid_el is None:
|
||||
print(f"[{category}] <yt:videoid> 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("/<path:filename>")
|
||||
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 version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
xml_string += '<feed xmlns:openSearch=\'http://a9.com/-/spec/opensearch/1.1/\' xmlns:media=\'http://search.yahoo.com/mrss/\' xmlns:yt=\'http://www.youtube.com/xml/schemas/2015\'>'
|
||||
xml_string += '<title type=\'text\'>Videos</title>'
|
||||
xml_string += '<author><name>ReviveMii</name><uri>http://revivemii.xyz</uri></author>'
|
||||
xml_string += '<generator ver=\'1.0\' uri=\'http://new.old.errexe.xyz/\'>RiiviveTube</generator>'
|
||||
xml_string += f'<openSearch:totalResults>{len(json_data)}</openSearch:totalResults>'
|
||||
xml_string += '<openSearch:startIndex>1</openSearch:startIndex>'
|
||||
xml_string += '<openSearch:itemsPerPage>20</openSearch:itemsPerPage>'
|
||||
|
||||
for item in json_data:
|
||||
xml_string += '<entry>'
|
||||
xml_string += '<id>http://127.0.0.1/api/videos/' + self.escape_xml(item["videoId"]) + '</id>'
|
||||
xml_string += '<published>' + self.escape_xml(item.get("publishedText", "")) + '</published>'
|
||||
xml_string += '<title type="text">' + self.escape_xml(item.get("title", "")) + '</title>'
|
||||
xml_string += '<link rel="http://127.0.0.1/api/videos/' + self.escape_xml(item["videoId"]) + '/related"/>'
|
||||
xml_string += '<author><name>' + self.escape_xml(item.get("author", "")) + '</name>'
|
||||
xml_string += '<uri>http://127.0.0.1/api/channels/' + self.escape_xml(item.get("authorId", "")) + '</uri></author>'
|
||||
xml_string += '<media:group>'
|
||||
xml_string += '<media:thumbnail yt:name="hqdefault" url="http://i.ytimg.com/vi/' + self.escape_xml(item["videoId"]) + '/hqdefault.jpg" height="240" width="320" time="00:00:00"/>'
|
||||
xml_string += '<yt:duration seconds="' + self.escape_xml(str(item.get("lengthSeconds", 0))) + '"/>'
|
||||
xml_string += '<yt:videoid id="' + self.escape_xml(item["videoId"]) + '">' + self.escape_xml(item["videoId"]) + '</yt:videoid>'
|
||||
xml_string += '<media:credit role="uploader" name="' + self.escape_xml(item.get("author", "")) + '">' + self.escape_xml(item.get("author", "")) + '</media:credit>'
|
||||
xml_string += '</media:group>'
|
||||
xml_string += '<yt:statistics favoriteCount="' + str(item.get("viewCount", 0)) + '" viewCount="' + str(item.get("viewCount", 0)) + '"/>'
|
||||
xml_string += '</entry>'
|
||||
|
||||
xml_string += '</feed>'
|
||||
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)
|
Loading…
x
Reference in New Issue
Block a user