Compare commits
20
Commits
f7477a4656
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c2593ff4 | ||
|
|
170914539d | ||
|
|
4060cb672f | ||
|
|
7acb9f63a3 | ||
|
|
0897b3002c | ||
|
|
b6d155f071 | ||
|
|
86d8283217 | ||
|
|
f01b246934 | ||
|
|
ed0eab672b | ||
|
|
d2f83ee5af | ||
|
|
cb9b7dabf8 | ||
|
|
3f5cc7e8de | ||
|
|
66144e92d4 | ||
|
|
fad252a099 | ||
|
|
4a73d2f388 | ||
|
|
c56340ffed | ||
|
|
cc58529e99 | ||
|
|
2e8a80c045 | ||
|
|
13e5d34446 | ||
|
|
0f6a62335f |
@@ -12,3 +12,6 @@ build/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
tmp
|
||||
Music
|
||||
cookies.txt
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Running with Docker
|
||||
TBD
|
||||
|
||||
# Dependences
|
||||
This section is only for people who do not want to use docker.
|
||||
|
||||
## Python Dependences
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Deno (JavaScript runtime for ytdlp)
|
||||
For Linux
|
||||
```
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
```
|
||||
|
||||
For Windows
|
||||
```
|
||||
irm https://deno.land/install.ps1 | iex
|
||||
```
|
||||
+466
-42
@@ -1,65 +1,489 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from threading import Thread
|
||||
import time
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.utils import DownloadError
|
||||
from dataclasses import dataclass
|
||||
from http.client import NETWORK_AUTHENTICATION_REQUIRED
|
||||
import pathlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import urllib.request
|
||||
from threading import Thread
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.postprocessor import PostProcessor
|
||||
from yt_dlp.utils import DownloadError, ExtractorError
|
||||
import mutagen
|
||||
from mutagen.easyid3 import EasyID3
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.id3 import ID3, APIC
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from ytmusicapi import YTMusic
|
||||
|
||||
MUSIC_FOLDER: str = os.environ.get("HAPPY_RIPPER_MUSIC_DIR", "Music")
|
||||
ACCESS_URL: str = os.environ.get("HAPPY_RIPPER_ACCESS_URL", "https://test.test.test")
|
||||
PORT = int(os.environ.get("HAPPY_RIPPER_MUSIC_PORT", 5000))
|
||||
DENO_PATH: str = os.environ.get("HAPPY_RIPPER_DENO_PATH", str(pathlib.Path.home() / ".deno/bin/deno"))
|
||||
|
||||
JS_RUNTIME_KEYS = {"deno": {"path": DENO_PATH}}
|
||||
|
||||
|
||||
def rip_worker(rip_queue: list[str]):
|
||||
@dataclass
|
||||
class ReleaseInfo:
|
||||
artist: str
|
||||
title: str
|
||||
art_url: str
|
||||
is_album: bool
|
||||
is_topic: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReleaseStatus:
|
||||
info: ReleaseInfo | None
|
||||
status: str
|
||||
destinations: set[str]
|
||||
|
||||
|
||||
class RipError(Exception):
|
||||
def __init__(self, msg):
|
||||
super().__init__(msg)
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class MetadataPostProcessor(PostProcessor):
|
||||
def __init__(self, info: ReleaseInfo, art_path: str, on_finish: Callable):
|
||||
super().__init__()
|
||||
self.info = info
|
||||
self.art_path = art_path
|
||||
self.on_finish = on_finish
|
||||
|
||||
def run(self, information):
|
||||
file = pathlib.Path(information["filepath"])
|
||||
|
||||
audio = EasyID3(file)
|
||||
audio.delete()
|
||||
audio["title"] = information["title"]
|
||||
audio["artist"] = information["uploader"].replace(" - Topic", "")
|
||||
|
||||
audio["album"] = self.info.title
|
||||
|
||||
index = information.get("playlist_index")
|
||||
if index is None:
|
||||
index = 1
|
||||
audio["tracknumber"] = str(index)
|
||||
|
||||
audio.save()
|
||||
|
||||
id3audio = MP3(file, ID3=ID3)
|
||||
id3audio_tags = id3audio.tags
|
||||
|
||||
if id3audio_tags is None:
|
||||
raise RipError("ID3 tags missing")
|
||||
|
||||
id3audio_tags.add(
|
||||
APIC(
|
||||
mime="image/jpeg",
|
||||
type=3,
|
||||
desc="Cover",
|
||||
data=open(self.art_path, "rb").read(),
|
||||
)
|
||||
)
|
||||
id3audio.save()
|
||||
|
||||
self.on_finish()
|
||||
return [], information
|
||||
|
||||
|
||||
def create_or_wipe_folder(path: str) -> None:
|
||||
"""Creates tmp directory, if it already exists, it clears it, and makes a new one."""
|
||||
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
else:
|
||||
shutil.rmtree(path)
|
||||
os.mkdir(path)
|
||||
|
||||
|
||||
def get_content_json(url: str):
|
||||
with YoutubeDL({"js_runtimes": JS_RUNTIME_KEYS, "playlist_items": "1"}) as ydl:
|
||||
return ydl.extract_info(url, download=False)
|
||||
|
||||
|
||||
def download_content(path: str, url: str, post_processor: PostProcessor) -> None:
|
||||
try:
|
||||
with YoutubeDL(
|
||||
{
|
||||
"js_runtimes": JS_RUNTIME_KEYS,
|
||||
"format": "bestaudio/best",
|
||||
"ignoreerrors": True,
|
||||
"restrictfilenames": True,
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
}
|
||||
],
|
||||
"outtmpl": f"{path}/%(title)s.%(ext)s",
|
||||
}
|
||||
) as ydl:
|
||||
ydl.add_post_processor(post_processor)
|
||||
error_code = ydl.download([url])
|
||||
if error_code > 1:
|
||||
raise RipError(f"Content download failed ({error_code})")
|
||||
|
||||
except DownloadError:
|
||||
raise RipError("Content Download Failed")
|
||||
|
||||
except ExtractorError:
|
||||
raise RipError("Content Extraction Failed")
|
||||
|
||||
|
||||
def find_release_info(url: str) -> ReleaseInfo:
|
||||
try:
|
||||
info = get_content_json(url)
|
||||
|
||||
except DownloadError:
|
||||
raise RipError("Info Download Failed")
|
||||
|
||||
except ExtractorError:
|
||||
raise RipError("Info Extraction Failed")
|
||||
|
||||
entries = info.get("entries")
|
||||
if entries is None:
|
||||
first_entry = info
|
||||
else:
|
||||
first_entry = next(iter(entries), None)
|
||||
if not first_entry:
|
||||
raise RipError("Entries is empty")
|
||||
|
||||
artist = first_entry.get("uploader")
|
||||
if artist is None:
|
||||
raise RipError("Uploader tag is none")
|
||||
|
||||
is_topic = False
|
||||
if " - Topic" in artist:
|
||||
is_topic = True
|
||||
artist = artist.replace(" - Topic", "")
|
||||
|
||||
title = info.get("title")
|
||||
if title is None:
|
||||
raise RipError("Title tag is none")
|
||||
|
||||
is_album = False
|
||||
if "Album - " in title:
|
||||
is_album = True
|
||||
title = title.replace("Album - ", "")
|
||||
|
||||
thumbnails = info.get("thumbnails")
|
||||
if thumbnails is None:
|
||||
raise RipError("Thumbnails is none")
|
||||
|
||||
art_url = thumbnails[1]["url"]
|
||||
|
||||
if not is_album:
|
||||
artist = "Various Artists"
|
||||
|
||||
return ReleaseInfo(artist, title, art_url, is_album, is_topic)
|
||||
|
||||
|
||||
def find_tmp_path() -> str:
|
||||
project_root_path = pathlib.Path(__file__).parent.resolve()
|
||||
tmp_path = f"{project_root_path}/tmp"
|
||||
return f"{project_root_path}/tmp"
|
||||
|
||||
|
||||
def sort_into_music(path: str, info: ReleaseInfo) -> set[str]:
|
||||
destinations: set[str] = set()
|
||||
|
||||
try:
|
||||
for file_name in os.listdir(path):
|
||||
if file_name == "cover.jpg":
|
||||
continue
|
||||
|
||||
audio = EasyID3(os.path.join(path, file_name))
|
||||
artist_list = audio.get("artist")
|
||||
if artist_list is None:
|
||||
raise RipError("Artist tag was None")
|
||||
|
||||
artist = artist_list[0]
|
||||
artist = re.sub(r"[^\w_. -]", "_", artist)
|
||||
title = info.title
|
||||
title = re.sub(r"[^\w_. -]", "_", title)
|
||||
|
||||
artist_path = os.path.join(MUSIC_FOLDER, artist)
|
||||
release_path = os.path.join(MUSIC_FOLDER, artist, title)
|
||||
|
||||
if not os.path.exists(artist_path):
|
||||
os.makedirs(artist_path)
|
||||
|
||||
if not os.path.exists(release_path):
|
||||
os.mkdir(release_path)
|
||||
|
||||
dst_file_path = os.path.join(release_path, file_name)
|
||||
dst_file_path_cover = os.path.join(release_path, "cover.jpg")
|
||||
|
||||
if os.path.exists(dst_file_path):
|
||||
os.remove(dst_file_path)
|
||||
|
||||
if os.path.exists(dst_file_path_cover):
|
||||
os.remove(dst_file_path_cover)
|
||||
|
||||
source_file_path = os.path.join(path, file_name)
|
||||
shutil.move(source_file_path, release_path)
|
||||
|
||||
source_file_path_cover = os.path.join(path, "cover.jpg")
|
||||
shutil.copy(source_file_path_cover, release_path)
|
||||
|
||||
destinations.add(release_path)
|
||||
|
||||
except Exception:
|
||||
raise RipError("Filesystem error")
|
||||
|
||||
return destinations
|
||||
|
||||
|
||||
def rip_worker(rip_queue: list[str], status_callback: Callable):
|
||||
tmp_path = find_tmp_path()
|
||||
|
||||
while True:
|
||||
queue_is_empty = len(rip_queue) == 0
|
||||
if queue_is_empty:
|
||||
if len(rip_queue) == 0:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# Creates tmp directory, if it already exists, it clears it, and makes a new one.
|
||||
if not os.path.isdir(tmp_path):
|
||||
os.mkdir(tmp_path)
|
||||
else:
|
||||
shutil.rmtree(tmp_path)
|
||||
os.mkdir(tmp_path)
|
||||
status_callback("Gathering info", False)
|
||||
|
||||
current_url = rip_queue[0]
|
||||
url = rip_queue.pop(0)
|
||||
create_or_wipe_folder(tmp_path)
|
||||
|
||||
try:
|
||||
with YoutubeDL(
|
||||
info = find_release_info(url)
|
||||
status_callback(info, False)
|
||||
|
||||
status_callback("Downloading art", False)
|
||||
art_path = os.path.join(tmp_path, "cover.jpg")
|
||||
urllib.request.urlretrieve(info.art_url, art_path)
|
||||
if not os.path.exists(art_path):
|
||||
raise RipError("Art download failed")
|
||||
|
||||
status_callback("Downloading content (0)", False)
|
||||
|
||||
on_index = 0
|
||||
|
||||
def on_download_finish() -> None:
|
||||
nonlocal on_index
|
||||
on_index += 1
|
||||
status_callback(f"Downloading content ({on_index})", False)
|
||||
|
||||
download_content(tmp_path, url, MetadataPostProcessor(info, art_path, on_download_finish))
|
||||
|
||||
filelist = os.listdir(tmp_path)
|
||||
filelist.remove("cover.jpg")
|
||||
if len(filelist) == 0:
|
||||
raise RipError("Nothing ripped, Try again")
|
||||
|
||||
status_callback("Sorting content", False)
|
||||
destinations = sort_into_music(tmp_path, info)
|
||||
status_callback(destinations, False)
|
||||
|
||||
except RipError as e:
|
||||
status_callback(e.msg, True)
|
||||
|
||||
except Exception:
|
||||
status_callback("Unknown Error", True)
|
||||
|
||||
else:
|
||||
status_callback("Finished sucessfully", True)
|
||||
|
||||
|
||||
def get_all_playlists_from_channel(browseId: str) -> list[str]:
|
||||
urls: list[str] = []
|
||||
yt = YTMusic()
|
||||
artist = yt.get_artist(browseId)
|
||||
albums_info = artist["albums"]
|
||||
|
||||
if "params" in albums_info:
|
||||
albums = yt.get_artist_albums(
|
||||
albums_info["browseId"],
|
||||
albums_info["params"],
|
||||
limit=None,
|
||||
)
|
||||
|
||||
for album in albums:
|
||||
playlist_id = album.get("playlistId")
|
||||
if playlist_id:
|
||||
urls.append(f"https://music.youtube.com/playlist?list={playlist_id}")
|
||||
|
||||
else:
|
||||
for album in artist["albums"]["results"]:
|
||||
playlist_id = album.get("audioPlaylistId")
|
||||
if playlist_id:
|
||||
urls.append(f"https://music.youtube.com/playlist?list={playlist_id}")
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = Flask(__name__)
|
||||
rip_queue: list[str] = []
|
||||
finished_queue: list[ReleaseStatus] = []
|
||||
|
||||
yt_music = YTMusic()
|
||||
|
||||
current_release_info: ReleaseInfo | None = None
|
||||
current_release_status_message: str = ""
|
||||
current_release_destinations: set[str] = set()
|
||||
|
||||
def get_current_release_status(x: ReleaseInfo | str | set[str], finished: bool):
|
||||
global current_release_info
|
||||
global current_release_status_message
|
||||
global current_release_destinations
|
||||
global finished_queue
|
||||
|
||||
if type(x) is ReleaseInfo:
|
||||
current_release_info = x
|
||||
|
||||
elif type(x) is str:
|
||||
current_release_status_message = x
|
||||
|
||||
elif type(x) is set:
|
||||
current_release_destinations = x.copy()
|
||||
|
||||
if finished:
|
||||
if current_release_info is None:
|
||||
current_release_info = ReleaseInfo("Unknown", "Unknown", "url", False, False)
|
||||
|
||||
finished_queue.append(
|
||||
ReleaseStatus(current_release_info, current_release_status_message, current_release_destinations.copy())
|
||||
)
|
||||
current_release_info = None
|
||||
current_release_status_message = "Waiting"
|
||||
current_release_destinations = set()
|
||||
|
||||
if len(finished_queue) > 30:
|
||||
finished_queue.pop(0)
|
||||
|
||||
ripping_thread = Thread(target=rip_worker, args=(rip_queue, get_current_release_status), daemon=True).start()
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.route("/browse")
|
||||
def browse():
|
||||
return render_template("browse.html")
|
||||
|
||||
@app.route("/rip", methods=["POST"])
|
||||
def rip():
|
||||
print("RIP")
|
||||
|
||||
url = request.form.get("url")
|
||||
if url is None:
|
||||
return jsonify({"success": False, "message": "Url is None"})
|
||||
|
||||
is_channel = request.form.get("is-channel-check")
|
||||
is_channel = is_channel is not None
|
||||
|
||||
if is_channel:
|
||||
channel_playlists = get_all_playlists_from_channel(url)
|
||||
|
||||
if len(channel_playlists) == 0:
|
||||
print("ERROR: Found no playlists in channel")
|
||||
return jsonify({"success": False, "message": "channel has no playlists"})
|
||||
|
||||
for playlist_url in channel_playlists:
|
||||
rip_queue.append(playlist_url)
|
||||
else:
|
||||
rip_queue.append(url)
|
||||
|
||||
return jsonify({"success": True, "message": f"Added, {url}!"})
|
||||
|
||||
@app.route("/status", methods=["GET"])
|
||||
def status():
|
||||
data = {"success": True, "queue": rip_queue, "finishedQueue": [], "status": current_release_status_message}
|
||||
|
||||
for q in finished_queue:
|
||||
if q.info is None:
|
||||
continue
|
||||
|
||||
data["finishedQueue"].append(
|
||||
{
|
||||
"format": "m4a/bestaudio/best",
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "m4a",
|
||||
}
|
||||
],
|
||||
"outtmpl": f"{tmp_path}/%(title)s.%(ext)s",
|
||||
"artist": q.info.artist,
|
||||
"title": q.info.title,
|
||||
"status": q.status,
|
||||
"art_url": q.info.art_url,
|
||||
"destinations": list(q.destinations),
|
||||
}
|
||||
) as ydl:
|
||||
error_code = ydl.download([current_url])
|
||||
)
|
||||
|
||||
except DownloadError:
|
||||
print("Download Error")
|
||||
if current_release_info is not None:
|
||||
data["currentReleaseInfo"] = {
|
||||
"artist": current_release_info.artist,
|
||||
"title": current_release_info.title,
|
||||
"art_url": current_release_info.art_url,
|
||||
}
|
||||
|
||||
rip_queue.pop(0)
|
||||
time.sleep(1)
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/search", methods=["POST"])
|
||||
def search():
|
||||
global yt_music
|
||||
|
||||
app = Flask(__name__)
|
||||
rip_queue = []
|
||||
ripping_thread = Thread(target=rip_worker, args=(rip_queue,), daemon=True).start()
|
||||
query = request.form.get("search")
|
||||
if query is None:
|
||||
return jsonify({"success": False, "message": "Search query is None"})
|
||||
|
||||
responce = yt_music.search(query)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
data = []
|
||||
for release in responce:
|
||||
release_data = {}
|
||||
|
||||
# Category ===============================================================
|
||||
category = release.get("resultType")
|
||||
if category is None:
|
||||
return jsonify({"success": False, "message": "Category is None"})
|
||||
|
||||
@app.route("/rip", methods=["POST"])
|
||||
def rip():
|
||||
url = request.form.get("url")
|
||||
rip_queue.append(url)
|
||||
return jsonify({"success": True, "message": f"Added, {url}!"})
|
||||
release_data["category"] = category
|
||||
|
||||
# Artist ===============================================================
|
||||
if "artist" in release:
|
||||
release_data["artist"] = release.get("artist", "Unknown")
|
||||
|
||||
elif "artists" in release:
|
||||
artists = release.get("artists")
|
||||
if artists is None:
|
||||
return jsonify({"success": False, "message": "Artists exists but is None"})
|
||||
|
||||
release_data["artist"] = artists[0].get("name", "Unknown")
|
||||
print(artists)
|
||||
|
||||
elif "author" in release:
|
||||
release_data["artist"] = release.get("author", "Unknown")
|
||||
|
||||
else:
|
||||
release_data["artist"] = "Unknown"
|
||||
|
||||
# Title ===============================================================
|
||||
if category == "artist":
|
||||
release_data["title"] = release_data["artist"]
|
||||
else:
|
||||
release_data["title"] = release.get("title", "Unknown")
|
||||
|
||||
# Thumbnails ===============================================================
|
||||
if "thumbnails" in release:
|
||||
release_data["thumbnails"] = release.get("thumbnails")
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Thumbnails is None"})
|
||||
|
||||
if "playlistId" in release:
|
||||
release_data["url"] = f"https://music.youtube.com/playlist?list={release['playlistId']}"
|
||||
elif "videoId" in release:
|
||||
release_data["url"] = f"https://music.youtube.com/watch?v={release['videoId']}"
|
||||
elif "browseId" in release and category == "artist":
|
||||
release_data["url"] = release.get("browseId")
|
||||
elif category == "artist" and "artists" in release:
|
||||
release_data["url"] = release.get("artists")[0].get("id")
|
||||
|
||||
data.append(release_data)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
app.run(host="0.0.0.0", port=PORT)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Flask==3.1.3
|
||||
mutagen==1.47.0
|
||||
yt_dlp==2026.8.19
|
||||
ytmusicapi==1.12.1
|
||||
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Happy Ripper</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Happy Ripper</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link active" aria-current="page" href="/">Queue</a>
|
||||
<a class="nav-link" href="/browse">Browse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="content">{% block content %}{% endblock %}</section>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<div class="container">
|
||||
<form id="browse-form">
|
||||
<div class="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="searchInput"
|
||||
placeholder="Album, Arist, Playlist..."
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
|
||||
<div class="container" id="results"></div>
|
||||
|
||||
<script>
|
||||
async function enqueue(url, is_artist) {
|
||||
const formData = new FormData();
|
||||
formData.append("url", url);
|
||||
|
||||
if (is_artist) {
|
||||
formData.append("is-channel-check", "");
|
||||
}
|
||||
|
||||
const response = await fetch("/rip", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
const results = document.getElementById("results");
|
||||
|
||||
document
|
||||
.getElementById("browse-form")
|
||||
.addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
const response = await fetch("/search", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
|
||||
results.innerHTML = "";
|
||||
for (release of data) {
|
||||
var button = "url" in release ? `
|
||||
<div class="col">
|
||||
<button class="btn btn-primary" onclick="enqueue('${release.url}', ${release.category == 'artist'})">
|
||||
Enqueue
|
||||
</button>
|
||||
</div>` : "";
|
||||
|
||||
results.innerHTML += `
|
||||
<div class="card" style="max-width: 720px;">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<img
|
||||
src="${release.thumbnails[0].url}"
|
||||
class="card-img-top"
|
||||
style="max-width: 120px;"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${release.title}</h5>
|
||||
<p class="card-text">${release.category} • ${release.artist}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${button}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
+152
-48
@@ -1,55 +1,159 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Happy Ripper</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<div class="container">
|
||||
<h2>Request</h2>
|
||||
<form id="rip-form">
|
||||
<div class="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
id="urlInput"
|
||||
placeholder="https://music.youtube.com/..."
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Request</h2>
|
||||
<form id="rip-form">
|
||||
<div class="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
id="urlInput"
|
||||
placeholder="https://music.youtube.com/..."
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is-channel-check" name="is-channel-check">
|
||||
<label class="form-check-label" for="is-channel-check">Is Channel</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Queue</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
|
||||
<div class="container">
|
||||
<h2>Current</h2>
|
||||
<div id="status-message"></div>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
|
||||
<div class="container">
|
||||
<h2>Queue</h2>
|
||||
<table class="table" id="queue-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queue-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
|
||||
<div class="container">
|
||||
<h2>Finished</h2>
|
||||
<div id=finish-table-body></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function createCard(artist, title, status, art) {
|
||||
return `
|
||||
<div class="card mb-3" style="max-width: 540px;">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<img src="${art}" class="img-fluid rounded-start">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${title}</h5>
|
||||
<p class="card-text">${artist}</p>
|
||||
<p class="card-text"><small class="text-body-secondary">${status}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Queue</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
<script>
|
||||
document
|
||||
.getElementById("rip-form")
|
||||
.addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
function createFinishCard(artist, title, status, art, destinations) {
|
||||
let dstList = ""
|
||||
for (destination of destinations) {
|
||||
dstList += `<li>${destination}</li>`;
|
||||
}
|
||||
|
||||
const formData = new FormData(this);
|
||||
let extraClasses = ""
|
||||
if (status !== "Finished sucessfully") {
|
||||
extraClasses = "border-danger"
|
||||
}
|
||||
|
||||
const response = await fetch("/rip", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
return `
|
||||
<div class="card mb-3 ${extraClasses}" style="max-width: 540px;">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<img src="${art}" class="img-fluid rounded-start">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${title}</h5>
|
||||
<p class="card-text">${artist}</p>
|
||||
|
||||
const data = await response.json();
|
||||
<ol>
|
||||
${dstList}
|
||||
</ol>
|
||||
|
||||
<p class="card-text"><small class="text-body-secondary">${status}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const ripFormInput = document.getElementById("urlInput");
|
||||
|
||||
document
|
||||
.getElementById("rip-form")
|
||||
.addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
const response = await fetch("/rip", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
</script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
const data = await response.json();
|
||||
ripFormInput.value = "";
|
||||
});
|
||||
|
||||
const queueTableBody = document.getElementById("queue-table-body");
|
||||
const finishTableBody = document.getElementById("finish-table-body");
|
||||
const statusMessage = document.getElementById("status-message");
|
||||
const statusMessageImage = document.getElementById("status-message-img");
|
||||
|
||||
async function status() {
|
||||
const response = await fetch("/status", { method: "GET" });
|
||||
const statusJson = await response.json();
|
||||
|
||||
if ("currentReleaseInfo" in statusJson) {
|
||||
const currentInfo = statusJson.currentReleaseInfo;
|
||||
statusMessage.innerHTML = createCard(currentInfo.artist, currentInfo.title, statusJson.status, currentInfo.art_url);
|
||||
}
|
||||
else {
|
||||
statusMessage.innerHTML = `<p>${statusJson.status}</p>`;
|
||||
}
|
||||
|
||||
queueTableBody.innerHTML = "";
|
||||
|
||||
var i = 1;
|
||||
for (const url of statusJson.queue) {
|
||||
queueTableBody.innerHTML += `<tr><td>${i}</td><td>${url}</td></tr>`;
|
||||
i ++;
|
||||
}
|
||||
|
||||
finishTableBody.innerHTML = "";
|
||||
for (const q of statusJson.finishedQueue.toReversed()) {
|
||||
finishTableBody.innerHTML += createFinishCard(q.artist, q.title, q.status, q.art_url, q.destinations);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(status, 100);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user