Compare commits
8
Commits
ed0eab672b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c2593ff4 | ||
|
|
170914539d | ||
|
|
4060cb672f | ||
|
|
7acb9f63a3 | ||
|
|
0897b3002c | ||
|
|
b6d155f071 | ||
|
|
86d8283217 | ||
|
|
f01b246934 |
@@ -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
|
||||||
|
```
|
||||||
+135
-10
@@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from http.client import NETWORK_AUTHENTICATION_REQUIRED
|
||||||
import pathlib
|
import pathlib
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -16,10 +17,14 @@ from mutagen.easyid3 import EasyID3
|
|||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from mutagen.id3 import ID3, APIC
|
from mutagen.id3 import ID3, APIC
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
from ytmusicapi import YTMusic
|
||||||
|
|
||||||
MUSIC_FOLDER = os.environ["HAPPY_RIPPER_MUSIC_DIR"]
|
MUSIC_FOLDER: str = os.environ.get("HAPPY_RIPPER_MUSIC_DIR", "Music")
|
||||||
ACCESS_URL = os.environ["HAPPY_RIPPER_ACCESS_URL"]
|
ACCESS_URL: str = os.environ.get("HAPPY_RIPPER_ACCESS_URL", "https://test.test.test")
|
||||||
PORT = int(os.environ["HAPPY_RIPPER_MUSIC_PORT"])
|
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}}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -57,7 +62,7 @@ class MetadataPostProcessor(PostProcessor):
|
|||||||
audio = EasyID3(file)
|
audio = EasyID3(file)
|
||||||
audio.delete()
|
audio.delete()
|
||||||
audio["title"] = information["title"]
|
audio["title"] = information["title"]
|
||||||
audio["artist"] = information["uploader"]
|
audio["artist"] = information["uploader"].replace(" - Topic", "")
|
||||||
|
|
||||||
audio["album"] = self.info.title
|
audio["album"] = self.info.title
|
||||||
|
|
||||||
@@ -99,7 +104,7 @@ def create_or_wipe_folder(path: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def get_content_json(url: str):
|
def get_content_json(url: str):
|
||||||
with YoutubeDL({"playlist_items": "1"}) as ydl:
|
with YoutubeDL({"js_runtimes": JS_RUNTIME_KEYS, "playlist_items": "1"}) as ydl:
|
||||||
return ydl.extract_info(url, download=False)
|
return ydl.extract_info(url, download=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +112,7 @@ def download_content(path: str, url: str, post_processor: PostProcessor) -> None
|
|||||||
try:
|
try:
|
||||||
with YoutubeDL(
|
with YoutubeDL(
|
||||||
{
|
{
|
||||||
|
"js_runtimes": JS_RUNTIME_KEYS,
|
||||||
"format": "bestaudio/best",
|
"format": "bestaudio/best",
|
||||||
"ignoreerrors": True,
|
"ignoreerrors": True,
|
||||||
"restrictfilenames": True,
|
"restrictfilenames": True,
|
||||||
@@ -173,6 +179,9 @@ def find_release_info(url: str) -> ReleaseInfo:
|
|||||||
|
|
||||||
art_url = thumbnails[1]["url"]
|
art_url = thumbnails[1]["url"]
|
||||||
|
|
||||||
|
if not is_album:
|
||||||
|
artist = "Various Artists"
|
||||||
|
|
||||||
return ReleaseInfo(artist, title, art_url, is_album, is_topic)
|
return ReleaseInfo(artist, title, art_url, is_album, is_topic)
|
||||||
|
|
||||||
|
|
||||||
@@ -195,9 +204,9 @@ def sort_into_music(path: str, info: ReleaseInfo) -> set[str]:
|
|||||||
raise RipError("Artist tag was None")
|
raise RipError("Artist tag was None")
|
||||||
|
|
||||||
artist = artist_list[0]
|
artist = artist_list[0]
|
||||||
re.sub(r"[^\w_. -]", "_", artist)
|
artist = re.sub(r"[^\w_. -]", "_", artist)
|
||||||
title = info.title
|
title = info.title
|
||||||
re.sub(r"[^\w_. -]", "_", title)
|
title = re.sub(r"[^\w_. -]", "_", title)
|
||||||
|
|
||||||
artist_path = os.path.join(MUSIC_FOLDER, artist)
|
artist_path = os.path.join(MUSIC_FOLDER, artist)
|
||||||
release_path = os.path.join(MUSIC_FOLDER, artist, title)
|
release_path = os.path.join(MUSIC_FOLDER, artist, title)
|
||||||
@@ -264,8 +273,11 @@ def rip_worker(rip_queue: list[str], status_callback: Callable):
|
|||||||
status_callback(f"Downloading content ({on_index})", False)
|
status_callback(f"Downloading content ({on_index})", False)
|
||||||
|
|
||||||
download_content(tmp_path, url, MetadataPostProcessor(info, art_path, on_download_finish))
|
download_content(tmp_path, url, MetadataPostProcessor(info, art_path, on_download_finish))
|
||||||
if len(os.listdir(tmp_path)) == 0:
|
|
||||||
raise RipError("No content was downloaded")
|
filelist = os.listdir(tmp_path)
|
||||||
|
filelist.remove("cover.jpg")
|
||||||
|
if len(filelist) == 0:
|
||||||
|
raise RipError("Nothing ripped, Try again")
|
||||||
|
|
||||||
status_callback("Sorting content", False)
|
status_callback("Sorting content", False)
|
||||||
destinations = sort_into_music(tmp_path, info)
|
destinations = sort_into_music(tmp_path, info)
|
||||||
@@ -281,11 +293,40 @@ def rip_worker(rip_queue: list[str], status_callback: Callable):
|
|||||||
status_callback("Finished sucessfully", True)
|
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__":
|
if __name__ == "__main__":
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
rip_queue: list[str] = []
|
rip_queue: list[str] = []
|
||||||
finished_queue: list[ReleaseStatus] = []
|
finished_queue: list[ReleaseStatus] = []
|
||||||
|
|
||||||
|
yt_music = YTMusic()
|
||||||
|
|
||||||
current_release_info: ReleaseInfo | None = None
|
current_release_info: ReleaseInfo | None = None
|
||||||
current_release_status_message: str = ""
|
current_release_status_message: str = ""
|
||||||
current_release_destinations: set[str] = set()
|
current_release_destinations: set[str] = set()
|
||||||
@@ -325,13 +366,33 @@ if __name__ == "__main__":
|
|||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.route("/browse")
|
||||||
|
def browse():
|
||||||
|
return render_template("browse.html")
|
||||||
|
|
||||||
@app.route("/rip", methods=["POST"])
|
@app.route("/rip", methods=["POST"])
|
||||||
def rip():
|
def rip():
|
||||||
|
print("RIP")
|
||||||
|
|
||||||
url = request.form.get("url")
|
url = request.form.get("url")
|
||||||
if url is None:
|
if url is None:
|
||||||
return jsonify({"success": False, "message": "Url is None"})
|
return jsonify({"success": False, "message": "Url is None"})
|
||||||
|
|
||||||
rip_queue.append(url)
|
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}!"})
|
return jsonify({"success": True, "message": f"Added, {url}!"})
|
||||||
|
|
||||||
@app.route("/status", methods=["GET"])
|
@app.route("/status", methods=["GET"])
|
||||||
@@ -361,4 +422,68 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
|
@app.route("/search", methods=["POST"])
|
||||||
|
def search():
|
||||||
|
global yt_music
|
||||||
|
|
||||||
|
query = request.form.get("search")
|
||||||
|
if query is None:
|
||||||
|
return jsonify({"success": False, "message": "Search query is None"})
|
||||||
|
|
||||||
|
responce = yt_music.search(query)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for release in responce:
|
||||||
|
release_data = {}
|
||||||
|
|
||||||
|
# Category ===============================================================
|
||||||
|
category = release.get("resultType")
|
||||||
|
if category is None:
|
||||||
|
return jsonify({"success": False, "message": "Category is None"})
|
||||||
|
|
||||||
|
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)
|
app.run(host="0.0.0.0", port=PORT)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
Flask==3.1.3
|
Flask==3.1.3
|
||||||
mutagen==1.47.0
|
mutagen==1.47.0
|
||||||
yt_dlp==2026.8.19
|
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 %}
|
||||||
+137
-152
@@ -1,174 +1,159 @@
|
|||||||
<!doctype html>
|
{% extends 'base.html' %} {% block content %}
|
||||||
<html lang="en">
|
<div class="container">
|
||||||
<head>
|
<h2>Request</h2>
|
||||||
<meta charset="UTF-8" />
|
<form id="rip-form">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<div class="mb-3">
|
||||||
<title>Happy Ripper</title>
|
<input
|
||||||
<link
|
type="text"
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
|
name="url"
|
||||||
rel="stylesheet"
|
id="urlInput"
|
||||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
|
placeholder="https://music.youtube.com/..."
|
||||||
crossorigin="anonymous"
|
class="form-control"
|
||||||
/>
|
required
|
||||||
</head>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<body>
|
<div class="mb-3 form-check">
|
||||||
<div class="container">
|
<input type="checkbox" class="form-check-input" id="is-channel-check" name="is-channel-check">
|
||||||
<h2>Request</h2>
|
<label class="form-check-label" for="is-channel-check">Is Channel</label>
|
||||||
<form id="rip-form">
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<input
|
<button type="submit" class="btn btn-primary">Queue</button>
|
||||||
type="text"
|
</form>
|
||||||
name="url"
|
</div>
|
||||||
id="urlInput"
|
|
||||||
placeholder="https://music.youtube.com/..."
|
</br>
|
||||||
class="form-control"
|
|
||||||
required
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Queue</button>
|
`;
|
||||||
</form>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
</br>
|
function createFinishCard(artist, title, status, art, destinations) {
|
||||||
|
let dstList = ""
|
||||||
|
for (destination of destinations) {
|
||||||
|
dstList += `<li>${destination}</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="container">
|
let extraClasses = ""
|
||||||
<h2>Current</h2>
|
if (status !== "Finished sucessfully") {
|
||||||
<div id="status-message"></div>
|
extraClasses = "border-danger"
|
||||||
</div>
|
}
|
||||||
|
|
||||||
</br>
|
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>
|
||||||
|
|
||||||
<div class="container">
|
<ol>
|
||||||
<h2>Queue</h2>
|
${dstList}
|
||||||
<table class="table" id="queue-table">
|
</ol>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">#</th>
|
|
||||||
<th scope="col">URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="queue-table-body"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</br>
|
<p class="card-text"><small class="text-body-secondary">${status}</small></p>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
}
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function createFinishCard(artist, title, status, art, destinations) {
|
const ripFormInput = document.getElementById("urlInput");
|
||||||
let dstList = ""
|
|
||||||
for (destination of destinations) {
|
|
||||||
dstList += `<li>${destination}</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let extraClasses = ""
|
document
|
||||||
if (status !== "Finished sucessfully") {
|
.getElementById("rip-form")
|
||||||
extraClasses = "border-danger"
|
.addEventListener("submit", async function (e) {
|
||||||
}
|
e.preventDefault();
|
||||||
|
|
||||||
return `
|
const formData = new FormData(this);
|
||||||
<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>
|
|
||||||
|
|
||||||
<ol>
|
const response = await fetch("/rip", {
|
||||||
${dstList}
|
method: "POST",
|
||||||
</ol>
|
body: formData,
|
||||||
|
|
||||||
<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,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
ripFormInput.value = "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const queueTableBody = document.getElementById("queue-table-body");
|
const data = await response.json();
|
||||||
const finishTableBody = document.getElementById("finish-table-body");
|
ripFormInput.value = "";
|
||||||
const statusMessage = document.getElementById("status-message");
|
});
|
||||||
const statusMessageImage = document.getElementById("status-message-img");
|
|
||||||
|
|
||||||
async function status() {
|
const queueTableBody = document.getElementById("queue-table-body");
|
||||||
const response = await fetch("/status", { method: "GET" });
|
const finishTableBody = document.getElementById("finish-table-body");
|
||||||
const statusJson = await response.json();
|
const statusMessage = document.getElementById("status-message");
|
||||||
|
const statusMessageImage = document.getElementById("status-message-img");
|
||||||
|
|
||||||
if ("currentReleaseInfo" in statusJson) {
|
async function status() {
|
||||||
const currentInfo = statusJson.currentReleaseInfo;
|
const response = await fetch("/status", { method: "GET" });
|
||||||
statusMessage.innerHTML = createCard(currentInfo.artist, currentInfo.title, statusJson.status, currentInfo.art_url);
|
const statusJson = await response.json();
|
||||||
}
|
|
||||||
else {
|
|
||||||
statusMessage.innerHTML = `<p>${statusJson.status}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueTableBody.innerHTML = "";
|
if ("currentReleaseInfo" in statusJson) {
|
||||||
|
const currentInfo = statusJson.currentReleaseInfo;
|
||||||
var i = 1;
|
statusMessage.innerHTML = createCard(currentInfo.artist, currentInfo.title, statusJson.status, currentInfo.art_url);
|
||||||
for (const url of statusJson.queue) {
|
}
|
||||||
queueTableBody.innerHTML += `<tr><td>${i}</td><td>${url}</td></tr>`;
|
else {
|
||||||
i ++;
|
statusMessage.innerHTML = `<p>${statusJson.status}</p>`;
|
||||||
}
|
|
||||||
|
|
||||||
finishTableBody.innerHTML = "";
|
|
||||||
for (const q of statusJson.finishedQueue) {
|
|
||||||
finishTableBody.innerHTML += createFinishCard(q.artist, q.title, q.status, q.art_url, q.destinations);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(status, 100);
|
queueTableBody.innerHTML = "";
|
||||||
</script>
|
|
||||||
|
|
||||||
<script
|
var i = 1;
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
for (const url of statusJson.queue) {
|
||||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
queueTableBody.innerHTML += `<tr><td>${i}</td><td>${url}</td></tr>`;
|
||||||
crossorigin="anonymous"
|
i ++;
|
||||||
></script>
|
}
|
||||||
</body>
|
|
||||||
</html>
|
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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user