Compare commits
No commits in common. "59b4d89f63ae181c7ebc3d50ba7ab1adb6798dbe" and "cd95e8b57e6f3dbda0c387880ac3ac65015bb9ea" have entirely different histories.
59b4d89f63
...
cd95e8b57e
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
token.txt
|
||||
.venv
|
||||
.idea
|
||||
__pycache__
|
||||
sigma
|
||||
nohup.out
|
||||
cookies.txt
|
16
LICENSE
16
LICENSE
@ -1,9 +1,15 @@
|
||||
MIT License
|
||||
(c) 2025 ReviveMii Project. All rights reserved. If you want to use this Code, give Credits to ReviveMii Project. https://revivemii.errexe.xyz/
|
||||
|
||||
Copyright (c) 2025 TheErrorExe
|
||||
ReviveMii Project and TheErrorExe is the Developer of this Code. Modification, Network Use and Distribution is allowed if you leave this Comment in the beginning of the Code, and if a website exist, Credits on the Website.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
This Code uses the Invidious API, Google API and yt-dlp. This Code is designed to run on Ubuntu 24.04.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Don’t claim that this code is your code. Don't use it without Credits to the ReviveMii Project. Don't use it without this Comment. Don't modify this Comment. You need to make your modified Code Open Source with this exact License.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
ReviveMii's Server Code is provided "as-is" and "as available." We do not guarantee uninterrupted access, error-free performance, or compatibility with all Wii systems. ReviveMii project is not liable for any damage, loss of data, or other issues arising from the use of this service and code.
|
||||
|
||||
If you use this Code, you agree to https://revivemii.errexe.xyz/revivetube/t-and-p.html, also available as http only Version: http://revivemii.errexe.xyz/revivetube/t-and-p.html
|
||||
|
||||
ReviveMii Project: https://revivemii.errexe.xyz/
|
||||
|
||||
If you are running your own public ReviveTube instance, you must open-source the modified source code.
|
||||
|
62
README.md
62
README.md
@ -1,3 +1,61 @@
|
||||
# modified-revivetube-for-ytolderrexexyz
|
||||
# ReviveTube
|
||||
|
||||
Modified Source Code for ReviveTube
|
||||
Watch YouTube on your Wii!
|
||||
|
||||
ReviveTube: http://yt.old.errexe.xyz/
|
||||
|
||||
ReviveMii Homepage: https://revivemii.errexe.xyz/
|
||||
|
||||
# Self Hosting
|
||||
|
||||
## Docker (Recommended):
|
||||
ReviveTube now supports Docker. Docker Installation:
|
||||
|
||||
Install docker with `sudo apt install docker.io && sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose`
|
||||
|
||||
|
||||
Then run
|
||||
```bash
|
||||
wget https://cloud.theerrorexe.dev/revivetube-docker.tar
|
||||
docker load -i revivetube-docker.tar
|
||||
```
|
||||
Create docker-compose.yml:
|
||||
```diff
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
revivetube:
|
||||
image: theerrorexe/revivetube:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- GOOGLE_API_TOKEN=YOUR_GOOGLE_TOKEN
|
||||
volumes:
|
||||
- .:/app
|
||||
restart: unless-stopped
|
||||
```
|
||||
Replace YOUR_GOOGLE_TOKEN with your google api token
|
||||
Now run `sudo docker-compose up -d` or `sudo docker compose up -d`
|
||||
|
||||
ReviveTube should now run on Port 5000
|
||||
|
||||
## Using Python directly
|
||||
|
||||
WARNING: before starting the server, remove the --proxy command and the --cookie command in revivetube.py
|
||||
|
||||
Go to https://console.cloud.google.com/ and create a new application with the YouTube Data v3 API.
|
||||
|
||||
Click on Credentials and click on new, and create a new API Key. Paste the API Key in token.txt
|
||||
|
||||
Install the Requirements:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
Search for "--proxy" in revivetube.py and remove the command
|
||||
|
||||
Search for "--cookies" in revivetube.py and remove the command
|
||||
|
||||
Start the Server:
|
||||
```bash
|
||||
python3 revivetube.py
|
||||
```
|
||||
|
43
check-folder-revivetube.py
Normal file
43
check-folder-revivetube.py
Normal file
@ -0,0 +1,43 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def get_folder_size(folder_path):
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(folder_path):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
if os.path.exists(filepath):
|
||||
total_size += os.path.getsize(filepath)
|
||||
return total_size
|
||||
|
||||
|
||||
def delete_files(folder_path, extensions):
|
||||
os.system('sudo pkill -f revivetube.py')
|
||||
process = subprocess.Popen(['sudo', 'nohup', 'python3', 'revivetube.py'])
|
||||
for dirpath, dirnames, filenames in os.walk(folder_path):
|
||||
for filename in filenames:
|
||||
if any(filename.lower().endswith(ext) for ext in extensions):
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
try:
|
||||
os.remove(filepath)
|
||||
except:
|
||||
print("ERROR")
|
||||
|
||||
|
||||
def monitor_folder(folder_path, size_limit_gb, check_interval):
|
||||
size_limit_bytes = size_limit_gb * 1024 * 1024 * 1024
|
||||
while True:
|
||||
folder_size = get_folder_size(folder_path)
|
||||
if folder_size > size_limit_bytes:
|
||||
delete_files(folder_path, [".flv", ".mp4"])
|
||||
time.sleep(check_interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
folder_to_monitor = "./sigma/videos/"
|
||||
size_limit = 7
|
||||
interval = 5
|
||||
|
||||
monitor_folder(folder_to_monitor, size_limit, interval)
|
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
1
generateRequirements.sh
Normal file
1
generateRequirements.sh
Normal file
@ -0,0 +1 @@
|
||||
pipreqs . --force --ignore .venv
|
57
helper.py
Normal file
57
helper.py
Normal file
@ -0,0 +1,57 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
async def read_file(path):
|
||||
try:
|
||||
async with aiofiles.open(path, 'r', encoding='utf-8') as file:
|
||||
return await file.read()
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {str(e)}")
|
||||
return None
|
||||
|
||||
async def get_video_duration_from_file(video_path):
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
'ffprobe', '-v', 'error', '-show_format',
|
||||
'-show_streams', '-of', 'json', video_path,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise Exception(stderr.decode())
|
||||
return float(json.loads(stdout.decode())['format']['duration'])
|
||||
except Exception as e:
|
||||
print(f"Error getting video duration: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
async def format_duration(seconds):
|
||||
return f"{int(seconds//60)}:{int(seconds%60):02d}"
|
||||
|
||||
async def get_file_size(file_path):
|
||||
try:
|
||||
return os.path.getsize(file_path)
|
||||
except Exception as e:
|
||||
print(f"Error getting file size: {str(e)}")
|
||||
return 0
|
||||
|
||||
async def get_range(file_path, byte_range):
|
||||
try:
|
||||
async with aiofiles.open(file_path, 'rb') as f:
|
||||
await f.seek(byte_range[0])
|
||||
return await f.read(byte_range[1] - byte_range[0] + 1)
|
||||
except Exception as e:
|
||||
print(f"Error reading file range: {str(e)}")
|
||||
return b''
|
||||
|
||||
async def get_api_key():
|
||||
try:
|
||||
async with aiofiles.open("token.txt", "r") as f:
|
||||
return (await f.read()).strip()
|
||||
except Exception as e:
|
||||
print(f"Error reading API key: {str(e)}")
|
||||
raise
|
BIN
likeicon.png
Normal file
BIN
likeicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 354 B |
BIN
loading.gif
Normal file
BIN
loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
player.swf
Normal file
BIN
player.swf
Normal file
Binary file not shown.
3
push.sh
Executable file
3
push.sh
Executable file
@ -0,0 +1,3 @@
|
||||
git add .
|
||||
git commit -m "something"
|
||||
git push -u origin main
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Flask
|
||||
Requests
|
||||
yt_dlp
|
||||
bs4
|
||||
quart
|
||||
aiohttp
|
||||
aiofiles
|
490
revivetube.py
Normal file
490
revivetube.py
Normal file
@ -0,0 +1,490 @@
|
||||
"""
|
||||
(c) 2025 ReviveMii Project. All rights reserved. If you want to use this Code, give Credits to ReviveMii Project. https://revivemii.errexe.xyz/
|
||||
|
||||
ReviveMii Project and TheErrorExe is the Developer of this Code. Modification, Network Use and Distribution is allowed if you leave this Comment in the beginning of the Code, and if a website exist, Credits on the Website.
|
||||
|
||||
This Code uses the Invidious API, Google API and yt-dlp. This Code is designed to run on Ubuntu 24.04.
|
||||
|
||||
Don't claim that this code is your code. Don't use it without Credits to the ReviveMii Project. Don't use it without this Comment. Don't modify this Comment. You need to make your modified Code Open Source with this exact License.
|
||||
|
||||
ReviveMii's Server Code is provided "as-is" and "as available." We do not guarantee uninterrupted access, error-free performance, or compatibility with all Wii systems. ReviveMii project is not liable for any damage, loss of data, or other issues arising from the use of this service and code.
|
||||
|
||||
If you use this Code, you agree to https://revivemii.errexe.xyz/revivetube/t-and-p.html, also available as http only Version: http://revivemii.errexe.xyz/revivetube/t-and-p.html
|
||||
|
||||
ReviveMii Project: https://revivemii.errexe.xyz/
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import yt_dlp
|
||||
from bs4 import BeautifulSoup
|
||||
from quart import Quart, request, render_template_string, send_file, Response, abort, jsonify
|
||||
from helper import *
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
VIDEO_FOLDER = "sigma/videos"
|
||||
YOUTUBE_API_URL = "https://www.googleapis.com/youtube/v3/videos"
|
||||
video_status = {}
|
||||
|
||||
FILE_SEPARATOR = os.sep
|
||||
|
||||
async def read_file(file_path):
|
||||
async with aiofiles.open(file_path, mode='r') as f:
|
||||
return await f.read()
|
||||
|
||||
async def check_and_create_folder():
|
||||
while True:
|
||||
folder_path = './sigma/videos'
|
||||
if not os.path.exists(folder_path):
|
||||
os.makedirs(folder_path)
|
||||
print(f"Folder {folder_path} got created.")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
@app.before_serving
|
||||
async def startup():
|
||||
asyncio.create_task(check_and_create_folder())
|
||||
|
||||
LOADING_TEMPLATE = None
|
||||
CHANNEL_TEMPLATE = None
|
||||
SEARCH_TEMPLATE = None
|
||||
INDEX_TEMPLATE = None
|
||||
WATCH_WII_TEMPLATE = None
|
||||
|
||||
async def load_templates():
|
||||
global LOADING_TEMPLATE, CHANNEL_TEMPLATE, SEARCH_TEMPLATE, INDEX_TEMPLATE, WATCH_WII_TEMPLATE
|
||||
LOADING_TEMPLATE = await read_file(f"site_storage{FILE_SEPARATOR}loading_template.html")
|
||||
CHANNEL_TEMPLATE = await read_file(f"site_storage{FILE_SEPARATOR}channel_template.html")
|
||||
SEARCH_TEMPLATE = await read_file(f"site_storage{FILE_SEPARATOR}search_template.html")
|
||||
INDEX_TEMPLATE = await read_file(f"site_storage{FILE_SEPARATOR}index_template.html")
|
||||
WATCH_WII_TEMPLATE = await read_file(f"site_storage{FILE_SEPARATOR}watch_wii_template.html")
|
||||
|
||||
app.before_serving(load_templates)
|
||||
|
||||
os.makedirs(VIDEO_FOLDER, exist_ok=True)
|
||||
|
||||
MAX_VIDEO_SIZE = 1 * 1024 * 1024 * 1024
|
||||
MAX_FOLDER_SIZE = 5 * 1024 * 1024 * 1024
|
||||
|
||||
def get_folder_size(path):
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
for f in filenames:
|
||||
file_path = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(file_path)
|
||||
return total_size
|
||||
|
||||
@app.route("/thumbnail/<video_id>")
|
||||
async def get_thumbnail(video_id):
|
||||
thumbnail_url = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(thumbnail_url) as response:
|
||||
if response.status == 200:
|
||||
image_data = await response.read()
|
||||
return Response(
|
||||
image_data,
|
||||
mimetype=response.headers.get("Content-Type", "image/jpeg")
|
||||
)
|
||||
return "Thumbnail not found", 404
|
||||
except Exception as e:
|
||||
print(f"Error fetching thumbnail: {str(e)}")
|
||||
return "Internal server error", 500
|
||||
|
||||
async def get_video_comments(video_id, max_results=20):
|
||||
api_key = await get_api_key()
|
||||
params = {
|
||||
"part": "snippet",
|
||||
"videoId": video_id,
|
||||
"key": api_key,
|
||||
"maxResults": max_results,
|
||||
"order": "relevance"
|
||||
}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://www.googleapis.com/youtube/v3/commentThreads", params=params, timeout=3) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
comments = []
|
||||
if "items" in data:
|
||||
for item in data["items"]:
|
||||
snippet = item["snippet"]["topLevelComment"]["snippet"]
|
||||
comments.append({
|
||||
"author": snippet["authorDisplayName"],
|
||||
"text": snippet["textDisplay"],
|
||||
"likeCount": snippet.get("likeCount", 0),
|
||||
"publishedAt": snippet["publishedAt"]
|
||||
})
|
||||
return comments
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
print(f"Can't fetch Comments: {str(e)}")
|
||||
return []
|
||||
|
||||
@app.route("/cookies.txt", methods=["GET"])
|
||||
async def cookies():
|
||||
return "403 Forbidden", 403
|
||||
|
||||
@app.route("/token.txt", methods=["GET"])
|
||||
async def token():
|
||||
return "403 Forbidden", 403
|
||||
|
||||
@app.route("/nohup.out", methods=["GET"])
|
||||
async def nohup():
|
||||
return "403 Forbidden", 403
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
async def index():
|
||||
query = request.args.get("query")
|
||||
results = None
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
url = f"https://invidious.errexe.xyz/api/v1/search?q={query}" if query else "https://invidious.errexe.xyz/api/v1/trending"
|
||||
async with session.get(url, timeout=6) as response:
|
||||
data = await response.json()
|
||||
if response.status == 200 and isinstance(data, list):
|
||||
if query:
|
||||
results = []
|
||||
for entry in data:
|
||||
if entry.get("type") == "video":
|
||||
results.append({
|
||||
"type": "video",
|
||||
"id": entry.get("videoId"),
|
||||
"title": entry.get("title"),
|
||||
"uploader": entry.get("author", "Unknown"),
|
||||
"thumbnail": f"/thumbnail/{entry['videoId']}",
|
||||
"viewCount": entry.get("viewCountText", "Unknown"),
|
||||
"published": entry.get("publishedText", "Unknown"),
|
||||
"duration": await format_duration(entry.get("lengthSeconds", 0))
|
||||
})
|
||||
elif entry.get("type") == "channel":
|
||||
results.append({
|
||||
"type": "channel",
|
||||
"id": entry.get("authorId"),
|
||||
"title": entry.get("author"),
|
||||
"thumbnail": entry.get("authorThumbnails")[-1]["url"] if entry.get("authorThumbnails") else "/static/default_channel_thumbnail.jpg",
|
||||
"subCount": entry.get("subCount", "Unknown"),
|
||||
"videoCount": entry.get("videoCount", "Unknown")
|
||||
})
|
||||
return await render_template_string(SEARCH_TEMPLATE, results=results)
|
||||
else:
|
||||
results = [
|
||||
{
|
||||
"id": entry.get("videoId"),
|
||||
"title": entry.get("title"),
|
||||
"uploader": entry.get("author", "Unknown"),
|
||||
"thumbnail": f"/thumbnail/{entry['videoId']}",
|
||||
"viewCount": entry.get("viewCountText", "Unknown"),
|
||||
"published": entry.get("publishedText", "Unknown"),
|
||||
"duration": await format_duration(entry.get("lengthSeconds", 0))
|
||||
}
|
||||
for entry in data
|
||||
if entry.get("videoId")
|
||||
]
|
||||
return await render_template_string(INDEX_TEMPLATE, results=results)
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError, ValueError) as e:
|
||||
return "Can't parse Data. If this Issue persists, report it in the Discord Server.", 500
|
||||
return "No Results or Error in the API.", 404
|
||||
|
||||
@app.route("/watch", methods=["GET"])
|
||||
async def watch():
|
||||
video_id = request.args.get("video_id")
|
||||
if not video_id:
|
||||
return "Missing Video-ID.", 400
|
||||
|
||||
video_mp4_path = os.path.join(VIDEO_FOLDER, f"{video_id}.mp4")
|
||||
video_flv_path = os.path.join(VIDEO_FOLDER, f"{video_id}.flv")
|
||||
|
||||
if video_id not in video_status:
|
||||
video_status[video_id] = {"status": "processing"}
|
||||
|
||||
user_agent = request.headers.get("User-Agent", "").lower()
|
||||
is_wii = "wii" in user_agent and "wiiu" not in user_agent
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"http://localhost:5000/video_metadata/{video_id}", timeout=20) as response:
|
||||
if response.status == 200:
|
||||
metadata = await response.json()
|
||||
else:
|
||||
return f"Metadata API Error for Video-ID {video_id}.", 500
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
return f"Can't connect to Metadata-API: {str(e)}", 500
|
||||
|
||||
comments = await get_video_comments(video_id)
|
||||
channel_logo_url = ""
|
||||
subscriber_count = "Unbekannt"
|
||||
|
||||
try:
|
||||
channel_id = metadata['channelId']
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"https://api-superplaycounts.onrender.com/api/youtube-channel-counter/user/{channel_id}", timeout=5) as channel_response:
|
||||
if channel_response.status == 200:
|
||||
channel_data = await channel_response.json()
|
||||
for stat in channel_data.get("statistics", []):
|
||||
for count in stat.get("counts", []):
|
||||
if count.get("value") == "subscribers":
|
||||
subscriber_count = count.get("count", "Unbekannt")
|
||||
break
|
||||
for user_info in stat.get("user", []):
|
||||
if user_info.get("value") == "pfp":
|
||||
channel_logo_url = user_info.get("count", "").replace("https://", "http://")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"SuperPlayCounts API Error: {str(e)}")
|
||||
|
||||
comment_count = len(comments)
|
||||
|
||||
if os.path.exists(video_mp4_path):
|
||||
video_duration = await get_video_duration_from_file(video_flv_path)
|
||||
alert_script = ""
|
||||
if video_duration > 420:
|
||||
alert_script = """<script type="text/javascript">alert("This Video is long. There is a chance that the Wii will not play the Video. Try a Video under 5 minutes.");</script>"""
|
||||
|
||||
if is_wii and os.path.exists(video_flv_path):
|
||||
return await render_template_string(WATCH_WII_TEMPLATE + alert_script,
|
||||
title=metadata['title'],
|
||||
uploader=metadata['uploader'],
|
||||
channelId=metadata['channelId'],
|
||||
description=metadata['description'].replace("\n", "<br>"),
|
||||
viewCount=metadata['viewCount'],
|
||||
likeCount=metadata['likeCount'],
|
||||
publishedAt=metadata['publishedAt'],
|
||||
comments=comments,
|
||||
commentCount=comment_count,
|
||||
channel_logo_url=channel_logo_url,
|
||||
subscriberCount=subscriber_count,
|
||||
video_id=video_id,
|
||||
video_flv=f"/sigma/videos/{video_id}.flv",
|
||||
alert_message="")
|
||||
|
||||
return await render_template_string(WATCH_WII_TEMPLATE,
|
||||
title=metadata['title'],
|
||||
uploader=metadata['uploader'],
|
||||
channelId=metadata['channelId'],
|
||||
description=metadata['description'].replace("\n", "<br>"),
|
||||
viewCount=metadata['viewCount'],
|
||||
likeCount=metadata['likeCount'],
|
||||
publishedAt=metadata['publishedAt'],
|
||||
comments=comments,
|
||||
commentCount=comment_count,
|
||||
channel_logo_url=channel_logo_url,
|
||||
subscriberCount=subscriber_count,
|
||||
video_id=video_id,
|
||||
video_flv=f"/sigma/videos/{video_id}.flv",
|
||||
alert_message="")
|
||||
|
||||
if not os.path.exists(video_mp4_path):
|
||||
if video_status[video_id]["status"] == "processing":
|
||||
asyncio.create_task(process_video(video_id))
|
||||
return await render_template_string(LOADING_TEMPLATE, video_id=video_id)
|
||||
|
||||
async def process_video(video_id):
|
||||
video_mp4_path = os.path.join(VIDEO_FOLDER, f"{video_id}.mp4")
|
||||
video_flv_path = os.path.join(VIDEO_FOLDER, f"{video_id}.flv")
|
||||
|
||||
try:
|
||||
video_status[video_id] = {"status": "downloading"}
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
|
||||
try:
|
||||
subprocess.run([
|
||||
"yt-dlp",
|
||||
"-o", os.path.join(temp_dir, f"{video_id}.%(ext)s"),
|
||||
"--cookies", "cookies.txt",
|
||||
"--proxy", "http://localhost:4000",
|
||||
"-f", "worstvideo+worstaudio",
|
||||
f"https://youtube.com/watch?v={video_id}"
|
||||
], check=True)
|
||||
|
||||
downloaded_files = [f for f in os.listdir(temp_dir) if video_id in f]
|
||||
if not downloaded_files:
|
||||
raise Exception("No video file downloaded")
|
||||
|
||||
downloaded_file = os.path.join(temp_dir, downloaded_files[0])
|
||||
|
||||
if not downloaded_file.endswith(".mp4"):
|
||||
video_status[video_id] = {"status": "converting"}
|
||||
try:
|
||||
subprocess.run([
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-i", downloaded_file,
|
||||
"-c:v", "libx264",
|
||||
"-crf", "51",
|
||||
"-c:a", "aac",
|
||||
"-strict", "experimental",
|
||||
"-preset", "ultrafast",
|
||||
"-b:a", "64k",
|
||||
"-movflags", "+faststart",
|
||||
"-vf", "scale=854:480",
|
||||
video_mp4_path
|
||||
], check=True, timeout=300, stderr=subprocess.PIPE)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("MP4 conversion timed out")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_output = e.stderr.decode('utf-8') if e.stderr else str(e)
|
||||
raise Exception(f"MP4 conversion failed: {error_output}")
|
||||
else:
|
||||
shutil.copy(downloaded_file, video_mp4_path)
|
||||
|
||||
if not os.path.exists(video_flv_path):
|
||||
video_status[video_id] = {"status": "converting for Wii"}
|
||||
try:
|
||||
subprocess.run([
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-i", video_mp4_path,
|
||||
"-ar", "22050",
|
||||
"-f", "flv",
|
||||
"-s", "320x240",
|
||||
"-ab", "32k",
|
||||
"-preset", "ultrafast",
|
||||
"-crf", "51",
|
||||
"-filter:v", "fps=fps=15",
|
||||
video_flv_path
|
||||
], check=True, timeout=300, stderr=subprocess.PIPE)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("FLV conversion timed out")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_output = e.stderr.decode('utf-8') if e.stderr else str(e)
|
||||
raise Exception(f"FLV conversion failed: {error_output}")
|
||||
|
||||
video_status[video_id] = {"status": "complete", "url": f"/sigma/videos/{video_id}.mp4"}
|
||||
|
||||
finally:
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
video_status[video_id] = {"status": "error", "message": error_msg}
|
||||
for path in [video_mp4_path, video_flv_path]:
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
except:
|
||||
pass
|
||||
|
||||
@app.route("/status/<video_id>")
|
||||
async def check_status(video_id):
|
||||
return jsonify(video_status.get(video_id, {"status": "pending"}))
|
||||
|
||||
@app.route("/video_metadata/<video_id>")
|
||||
async def video_metadata(video_id):
|
||||
api_key = await get_api_key()
|
||||
params = {
|
||||
"part": "snippet,statistics",
|
||||
"id": video_id,
|
||||
"key": api_key
|
||||
}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(YOUTUBE_API_URL, params=params, timeout=1) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
if "items" not in data or len(data["items"]) == 0:
|
||||
return f"The Video with ID {video_id} was not found.", 404
|
||||
video_data = data["items"][0]
|
||||
return {
|
||||
"title": video_data["snippet"]["title"],
|
||||
"uploader": video_data["snippet"]["channelTitle"],
|
||||
"channelId": video_data["snippet"]["channelId"],
|
||||
"description": video_data["snippet"]["description"],
|
||||
"viewCount": video_data["statistics"].get("viewCount", "Unknown"),
|
||||
"likeCount": video_data["statistics"].get("likeCount", "Unknown"),
|
||||
"dislikeCount": video_data["statistics"].get("dislikeCount", "Unknown"),
|
||||
"publishedAt": video_data["snippet"].get("publishedAt", "Unknown")
|
||||
}
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
return f"API Error: {str(e)}", 500
|
||||
|
||||
@app.route("/<path:filename>")
|
||||
async def serve_video(filename):
|
||||
file_path = os.path.join(filename)
|
||||
if not os.path.exists(file_path):
|
||||
return "File not found.", 404
|
||||
|
||||
file_size = await get_file_size(file_path)
|
||||
range_header = request.headers.get('Range', None)
|
||||
if range_header:
|
||||
byte_range = range_header.strip().split('=')[1]
|
||||
start_byte, end_byte = byte_range.split('-')
|
||||
start_byte = int(start_byte)
|
||||
end_byte = int(end_byte) if end_byte else file_size - 1
|
||||
|
||||
if start_byte >= file_size or end_byte >= file_size:
|
||||
abort(416)
|
||||
|
||||
data = await get_range(file_path, (start_byte, end_byte))
|
||||
content_range = f"bytes {start_byte}-{end_byte}/{file_size}"
|
||||
|
||||
response = Response(
|
||||
data,
|
||||
status=206,
|
||||
mimetype="video/mp4",
|
||||
content_type="video/mp4",
|
||||
)
|
||||
response.headers["Content-Range"] = content_range
|
||||
response.headers["Content-Length"] = str(len(data))
|
||||
return response
|
||||
|
||||
return await send_file(file_path)
|
||||
|
||||
@app.route('/channel', methods=['GET'])
|
||||
async def channel_m():
|
||||
channel_id = request.args.get('channel_id', None)
|
||||
if not channel_id:
|
||||
return "Channel ID is required.", 400
|
||||
|
||||
ydl_opts = {
|
||||
'quiet': True,
|
||||
'extract_flat': True,
|
||||
'playlistend': 20,
|
||||
}
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(f"https://www.youtube.com/channel/{channel_id}/videos", download=False)
|
||||
if 'entries' not in info:
|
||||
return "No videos found.", 404
|
||||
channel_name = info.get('uploader', 'Unknown')
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"https://invidious.errexe.xyz/channel/{channel_id}", timeout=10) as response:
|
||||
if response.status != 200:
|
||||
return "Failed to fetch channel page.", 500
|
||||
soup = BeautifulSoup(await response.text(), "html.parser")
|
||||
profile_div = soup.find(class_="channel-profile")
|
||||
channel_picture = ""
|
||||
if profile_div:
|
||||
img_tag = profile_div.find("img")
|
||||
if img_tag and "src" in img_tag.attrs:
|
||||
channel_picture = f"http://api.allorigins.win/raw?url=http://invidious.materialio.us{img_tag['src']}"
|
||||
|
||||
results = [
|
||||
{
|
||||
'id': video['id'],
|
||||
'duration': 'Duration not available on Channel View',
|
||||
'title': video['title'],
|
||||
'uploader': channel_name,
|
||||
'thumbnail': f"http://yt.old.errexe.xyz/thumbnail/{video['id']}"
|
||||
}
|
||||
for video in info['entries']
|
||||
]
|
||||
return await render_template_string(
|
||||
CHANNEL_TEMPLATE,
|
||||
results=results,
|
||||
channel_name=channel_name,
|
||||
channel_picture=channel_picture
|
||||
)
|
||||
except Exception as e:
|
||||
return f"An error occurred: {str(e)}", 500
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
134
site_storage/channel_template.html
Normal file
134
site_storage/channel_template.html
Normal file
@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c709974b13b14d9a8a5c19c9cb3e5184"}'></script><!-- End Cloudflare Web Analytics -->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>ReviveTube - A YouTube App for the Wii</title>
|
||||
<style>
|
||||
a {
|
||||
color: #3ea6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #46bbff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
background-color: #181818;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom:2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border:1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.video-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.video-item {
|
||||
width: 320px;
|
||||
margin: 10px;
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
}
|
||||
.video-item img {
|
||||
width: 100%;
|
||||
}
|
||||
.video-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.video-item-uploader, .video-item-duration {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="/" style="text-decoration: none;"><div class="logo"><img src="../favicon.ico" style="width:32px; height:32px; display:inline; position:relative; top:3px; right:3px; padding-right:2px;"><span style="position:relative; top:-4px; padding-left:5px; border-left:1px solid #323232;">
|
||||
ReviveTube</span></div></a>
|
||||
<div class="search-container">
|
||||
<form action="/" method="get">
|
||||
<input class="search-bar" placeholder="Search YouTube" name="query" type="text">
|
||||
<input type="submit" class="search-button" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% if results %}
|
||||
<div>
|
||||
<img src="{{ channel_picture if channel_picture else 'default_profile.png' }}"
|
||||
alt="Channel Profile Picture"
|
||||
style="width: 48px; height: 48px; border-radius: 50%; object-fit: cover; display: inline-block; vertical-align: middle; margin-right: 10px;">
|
||||
<h2 style="display: inline-block; vertical-align: middle; margin: 0; color: white;">{{ channel_name }}</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="video-grid">
|
||||
{% for video in results %}
|
||||
<div class="video-item">
|
||||
<a href="/watch?video_id={{ video['id'] }}">
|
||||
<img alt="{{ video['title'] }}" src="{{ video['thumbnail'] }}">
|
||||
</a>
|
||||
<div class="video-item-title">{{ video['title'] }}</div>
|
||||
<div class="video-item-uploader">By: {{ video['uploader'] }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p style="color: red; text-align: center;">ReviveTube - A YouTube App for the Wii</p>
|
||||
<p style="text-align: center;"><a href="http://revivemii.xyz" target="_blank">Visit the ReviveMii Project</a></p>
|
||||
<p style="font-size: 12px; text-align: center;">We are NOT affiliated with Nintendo or YouTube.</p>
|
||||
<p style="text-align: center;">
|
||||
<a href="https://github.com/ReviveMii/revivetube/" target="_blank">Source Code</a> |
|
||||
<a href="https://revivemii.errexe.xyz/discord-redirect.html">Discord Server</a> |
|
||||
<a href="mailto:theerrorexe@gmail.com">Contact</a> |
|
||||
<a href="/site_storage/credits.html">Credits</a> |
|
||||
<a href="mailto:dmca@errexe.xyz">DMCA: dmca@errexe.xyz</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
118
site_storage/credits.html
Normal file
118
site_storage/credits.html
Normal file
@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c709974b13b14d9a8a5c19c9cb3e5184"}'></script><!-- End Cloudflare Web Analytics -->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>ReviveTube - A YouTube App for the Wii</title>
|
||||
<style>
|
||||
a {
|
||||
color: #3ea6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #46bbff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
background-color: #181818;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom:2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border:1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.video-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.video-item {
|
||||
width: 320px;
|
||||
margin: 10px;
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
}
|
||||
.video-item img {
|
||||
width: 100%;
|
||||
}
|
||||
.video-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.video-item-uploader, .video-item-duration {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo"><img src="../favicon.ico" style="width:32px; height:32px; display:inline; position:relative; top:3px; right:3px; padding-right:2px;"><span style="position:relative; top:-4px; padding-left:5px; border-left:1px solid #323232;">
|
||||
ReviveTube</span></div>
|
||||
<div class="search-container">
|
||||
<form action="/" method="get">
|
||||
<input class="search-bar" placeholder="Search YouTube" name="query" type="text">
|
||||
<input type="submit" class="search-button" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1>Credits:</h1>
|
||||
<p>TheErrorExe (Developer)</p>
|
||||
<p>Chrisplayz (Credits for the Redesign)</p>
|
||||
<p>rmilooo (Credits for the Code Cleanup)</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: red; text-align: center;">ReviveTube - A YouTube App for the Wii</p>
|
||||
<p style="text-align: center;"><a href="http://revivemii.xyz" target="_blank">Visit the ReviveMii Project</a></p>
|
||||
<p style="font-size: 12px; text-align: center;">We are NOT affiliated with Nintendo or YouTube.</p>
|
||||
<p style="text-align: center;">
|
||||
<a href="https://github.com/ReviveMii/revivetube/" target="_blank">Source Code</a> |
|
||||
<a href="https://revivemii.errexe.xyz/discord-redirect.html">Discord Server</a> |
|
||||
<a href="mailto:theerrorexe@gmail.com">Contact</a> |
|
||||
<a href="/site_storage/credits.html">Credits</a> |
|
||||
<a href="mailto:dmca@errexe.xyz">DMCA: dmca@errexe.xyz</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
131
site_storage/index_template.html
Normal file
131
site_storage/index_template.html
Normal file
@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c709974b13b14d9a8a5c19c9cb3e5184"}'></script><!-- End Cloudflare Web Analytics -->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>ReviveTube - A YouTube App for the Wii</title>
|
||||
<style>
|
||||
a {
|
||||
color: #3ea6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #46bbff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
background-color: #181818;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom:2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border:1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.video-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.video-item {
|
||||
width: 320px;
|
||||
margin: 10px;
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
}
|
||||
.video-item img {
|
||||
width: 100%;
|
||||
}
|
||||
.video-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.video-item-uploader, .video-item-duration {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="/" style="text-decoration: none;"><div class="logo"><img src="../favicon.ico" style="width:32px; height:32px; display:inline; position:relative; top:3px; right:3px; padding-right:2px;"><span style="position:relative; top:-4px; padding-left:5px; border-left:1px solid #323232;">
|
||||
ReviveTube</span></div></a>
|
||||
<div class="search-container">
|
||||
<form action="/" method="get">
|
||||
<input class="search-bar" placeholder="Search YouTube" name="query" type="text">
|
||||
<input type="submit" class="search-button" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<!-- <div style="background-color: lightblue; border-radius: 25px; padding: 20px;">
|
||||
<h1>We now have a Wii Forwarder for ReviveTube!</h1>
|
||||
<a href="https://go.revivemii.xyz/revivetube-forwarder.html">Click here (Open this on a Modern Device)</a>
|
||||
</div> -->
|
||||
{% if results %}
|
||||
<div class="video-grid">
|
||||
{% for video in results %}
|
||||
<div class="video-item">
|
||||
<a href="/watch?video_id={{ video['id'] }}">
|
||||
<img alt="{{ video['title'] }}" src="{{ video['thumbnail'] }}">
|
||||
</a>
|
||||
<div class="video-item-title">{{ video['title'] }}</div>
|
||||
<div class="video-item-uploader">By: {{ video['uploader'] }}</div>
|
||||
<div class="video-item-duration">Duration: {{ video['duration'] }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p style="color: red; text-align: center;">ReviveTube - A YouTube App for the Wii</p>
|
||||
<p style="text-align: center;"><a href="http://revivemii.xyz" target="_blank">Visit the ReviveMii Project</a></p>
|
||||
<p style="font-size: 12px; text-align: center;">We are NOT affiliated with Nintendo or YouTube.</p>
|
||||
<p style="text-align: center;">
|
||||
<a href="https://github.com/ReviveMii/revivetube/" target="_blank">Source Code</a> |
|
||||
<a href="https://revivemii.errexe.xyz/discord-redirect.html">Discord Server</a> |
|
||||
<a href="mailto:theerrorexe@gmail.com">Contact</a> |
|
||||
<a href="/site_storage/credits.html">Credits</a> |
|
||||
<a href="mailto:dmca@errexe.xyz">DMCA: dmca@errexe.xyz</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
162
site_storage/loading_template.html
Normal file
162
site_storage/loading_template.html
Normal file
@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c709974b13b14d9a8a5c19c9cb3e5184"}'></script><!-- End Cloudflare Web Analytics -->
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<title>Loading...</title>
|
||||
<style>
|
||||
a {
|
||||
color: #3ea6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #46bbff;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
background-color: #0f0f0f;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom:2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border:1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.loading-section {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
#loadingGif {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
#goButton {
|
||||
display: none;
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
#goButton:disabled {
|
||||
background-color: gray;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
small {
|
||||
color: grey;
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="/" style="text-decoration: none;"><div class="logo"><img src="../favicon.ico" style="width:32px; height:32px; display:inline; position:relative; top:3px; right:3px; padding-right:2px;"><span style="position:relative; top:-4px; padding-left:5px; border-left:1px solid #323232;">
|
||||
ReviveTube</span></div></a>
|
||||
<div class="search-container">
|
||||
<form action="/" method="get">
|
||||
<input class="search-bar" name="query" placeholder="Search YouTube" type="text">
|
||||
<input type="submit" class="search-button" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-section">
|
||||
<h1>Loading...</h1>
|
||||
<img alt="Loading..." id="loadingGif" src="loading.gif"/>
|
||||
<p id="progressText">Fetching Info...</p>
|
||||
<button id="goButton" onclick="startVideo()">Go</button>
|
||||
<br>
|
||||
<small>Loading Screen will <b>NOT</b> work in Dolphin Emulator.<br><br>Long Video = Longer Download and Converting.<br><br>Videos over 5 minutes will not play.</small>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var goButton = document.getElementById('goButton');
|
||||
var loadingGif = document.getElementById('loadingGif');
|
||||
var progressText = document.getElementById('progressText');
|
||||
var videoId = "{{ video_id }}";
|
||||
|
||||
function simulateLoading() {
|
||||
setInterval(checkStatus, 1000);
|
||||
}
|
||||
|
||||
|
||||
function checkStatus() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/status/' + videoId, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
var response;
|
||||
try {
|
||||
response = eval('(' + xhr.responseText + ')');
|
||||
} catch (e) {
|
||||
response = { status: 'error' };
|
||||
}
|
||||
updateProgress(response);
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function updateProgress(status) {
|
||||
if (status.status === 'complete') {
|
||||
window.location.href = '/watch?video_id=' + videoId;
|
||||
} else if (status.status === 'downloading') {
|
||||
progressText.innerHTML = 'Downloading...';
|
||||
} else if (status.status === 'converting') {
|
||||
progressText.innerHTML = 'Converting video from mp4 to webm...';
|
||||
} else if (status.status === 'converting for Wii') {
|
||||
progressText.innerHTML = 'Converting to Flash Video...';
|
||||
} else {
|
||||
progressText.innerHTML = 'The Server was unable to process the video! Report the Bug in the Discord Server. <br> Error on Video with ID: {{ video_id }}<br>Discord Server: https://revivemii.xyz/discord-redirect';
|
||||
}
|
||||
}
|
||||
|
||||
function startVideo() {
|
||||
window.location.href = '/watch?video_id=' + videoId;
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
simulateLoading();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
131
site_storage/search_template.html
Normal file
131
site_storage/search_template.html
Normal file
@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c709974b13b14d9a8a5c19c9cb3e5184"}'></script><!-- End Cloudflare Web Analytics -->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>ReviveTube - A YouTube App for the Wii</title>
|
||||
<style>
|
||||
a {
|
||||
color: #3ea6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #46bbff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
background-color: #181818;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom:2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border:1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.video-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.video-item {
|
||||
width: 320px;
|
||||
margin: 10px;
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
}
|
||||
.video-item img {
|
||||
width: 100%;
|
||||
}
|
||||
.video-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.video-item-uploader, .video-item-duration {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="/" style="text-decoration: none;"><div class="logo"><img src="../favicon.ico" style="width:32px; height:32px; display:inline; position:relative; top:3px; right:3px; padding-right:2px;"><span style="position:relative; top:-4px; padding-left:5px; border-left:1px solid #323232;">
|
||||
ReviveTube</span></div></a>
|
||||
<div class="search-container">
|
||||
<form action="/" method="get">
|
||||
<input class="search-bar" placeholder="Search YouTube" name="query" type="text">
|
||||
<input type="submit" class="search-button" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% if results %}
|
||||
<div class="video-grid">
|
||||
{% for result in results %}
|
||||
<div class="video-item">
|
||||
<a href="{% if result.type == 'video' %}/watch?video_id={{ result.id }}{% else %}/channel?channel_id={{ result.id }}{% endif %}">
|
||||
<img alt="{{ result.title }}" src="{{ result.thumbnail }}">
|
||||
</a>
|
||||
<div class="video-item-title">{{ result.title }}</div>
|
||||
{% if result.type == 'video' %}
|
||||
<div class="video-item-uploader">By: {{ result.uploader }}</div>
|
||||
<div class="video-item-duration">Duration: {{ result.duration }}</div>
|
||||
{% else %}
|
||||
<div class="video-item-uploader">Subscribers: {{ result.subCount }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p style="color: red; text-align: center;">ReviveTube - A YouTube App for the Wii</p>
|
||||
<p style="text-align: center;"><a href="http://revivemii.xyz" target="_blank">Visit the ReviveMii Project</a></p>
|
||||
<p style="font-size: 12px; text-align: center;">We are NOT affiliated with Nintendo or YouTube.</p>
|
||||
<p style="text-align: center;">
|
||||
<a href="https://github.com/ReviveMii/revivetube/" target="_blank">Source Code</a> |
|
||||
<a href="https://revivemii.errexe.xyz/discord-redirect.html">Discord Server</a> |
|
||||
<a href="mailto:theerrorexe@gmail.com">Contact</a> |
|
||||
<a href="/site_storage/credits.html">Credits</a> |
|
||||
<a href="mailto:dmca@errexe.xyz">DMCA: dmca@errexe.xyz</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
222
site_storage/watch_wii_template.html
Normal file
222
site_storage/watch_wii_template.html
Normal file
@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c709974b13b14d9a8a5c19c9cb3e5184"}'></script><!-- End Cloudflare Web Analytics -->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<style>
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom:2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border:1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
a {
|
||||
color: #3ea6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #46bbff;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
background-color: #0f0f0f;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background-color: #202020;
|
||||
border-bottom: 2px solid #2c2c2c;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
color: #fe0000;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.logo img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #313131;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.search-bar:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.search-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #222222;
|
||||
border: 1px solid #3d3d3d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-button:hover {
|
||||
border: 1px solid #268ee9;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.channel-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
.channel-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.like-image {
|
||||
width: 20px;
|
||||
height: auto;
|
||||
}
|
||||
#videooutline {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
border-bottom: 6px solid #181818;
|
||||
border-right: 6px solid #181818;
|
||||
}
|
||||
#videooutline:hover {
|
||||
border-bottom: 6px solid #111111;
|
||||
border-right: 6px solid #111111;
|
||||
}
|
||||
.video-container {
|
||||
background-color: #131313;
|
||||
border: 1px solid #525252;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
.video-container:hover {
|
||||
border: 1px solid #0f0f0f;
|
||||
}
|
||||
.comments-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.comment {
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.comment-author {
|
||||
font-weight: bold;
|
||||
color: #3ea6ff;
|
||||
}
|
||||
.comment-text {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.comment-likes {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
<script src="https://unpkg.com/@ruffle-rs/ruffle"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="/" style="text-decoration: none;"><div class="logo"><img src="../favicon.ico" style="width:32px; height:32px; display:inline; position:relative; top:3px; right:3px; padding-right:2px;"><span style="position:relative; top:-4px; padding-left:5px; border-left:1px solid #323232;">
|
||||
ReviveTube</span></div></a>
|
||||
<div class="search-container">
|
||||
<form action="/" method="get">
|
||||
<input class="search-bar" name="query" placeholder="Search YouTube" type="text">
|
||||
<input type="submit" class="search-button" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div id="videooutline">
|
||||
<div class="video-container">
|
||||
<object data="/player.swf" height="256" type="application/x-shockwave-flash" width="384">
|
||||
<param name="wmode" value="transparent">
|
||||
<param name="allowFullScreen" value="false">
|
||||
<param name="flashvars" value="filename={{ video_flv }}">
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h1 class="video-title">{{ title }}</h1>
|
||||
<div class="video-meta">{{ viewCount }} Views • {{ publishedAt }}</div>
|
||||
<br>
|
||||
<div class="channel-info">
|
||||
<img src="{{ channel_logo_url }}" alt="Channel Logo" class="channel-logo">
|
||||
<div>
|
||||
<a href="/channel?channel_id={{ channelId }}">{{ uploader }}</a>
|
||||
<div class="subscriber-count">{{ subscriberCount }} Subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="like-container">
|
||||
<img src="/likeicon.png" alt="Like" class="like-image">
|
||||
<span>{{ likeCount }}</span>
|
||||
</div><br>
|
||||
<div class="comments-section">
|
||||
<h2>Comments ({{ commentCount }})</h2>
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
<div class="comment">
|
||||
<div class="comment-author">{{ comment.author }}</div>
|
||||
<div class="comment-text">{{ comment.text | safe}}</div>
|
||||
<div class="comment-likes">👍 {{ comment.likeCount }} | {{ comment.publishedAt }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No Comments ):</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user