first commit
This commit is contained in:
commit
233b64a51d
14 changed files with 936 additions and 0 deletions
100
grab.py
Normal file
100
grab.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SHOWS_ROOT = Path(__file__).resolve().parent
|
||||
DOWNLOADS = SHOWS_ROOT / ".downloads"
|
||||
SEED_RATIO = 2.0
|
||||
SEED_TIME_MIN = 1440
|
||||
PORT = 6881
|
||||
VIDEO_EXTS = {'.mkv', '.mp4', '.avi', '.mov', '.m4v', '.ts', '.webm'}
|
||||
|
||||
def extract_ep(name):
|
||||
stem = Path(name).stem
|
||||
stem = re.sub(r'^\[[^\]]+\]\s*', '', stem)
|
||||
stem = re.sub(r'\([^)]*\)', '', stem)
|
||||
stem = re.sub(r'\[[^\]]*\]', '', stem)
|
||||
if re.search(r'\b(NCOP|NCED|OP|ED|Menu|Extra|Preview|PV|SP|Special)\b', stem, re.I):
|
||||
return None
|
||||
m = re.search(r'(?:^|\s|-)\s*(\d{1,3})(?:v\d)?(?=\s|$|\.)', stem)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
def organize(stage, name, year, season):
|
||||
show_dir = SHOWS_ROOT / f"{name} ({year})"
|
||||
season_dir = show_dir / f"Season {int(season):02d}"
|
||||
specials_dir = show_dir / "Specials"
|
||||
season_dir.mkdir(parents=True, exist_ok=True)
|
||||
for f in Path(stage).rglob('*'):
|
||||
if not f.is_file() or f.suffix.lower() not in VIDEO_EXTS:
|
||||
continue
|
||||
ep = extract_ep(f.name)
|
||||
if ep is None:
|
||||
specials_dir.mkdir(parents=True, exist_ok=True)
|
||||
dst = specials_dir / f.name
|
||||
else:
|
||||
dst = season_dir / f"{name} - S{int(season):02d}E{ep:02d}{f.suffix.lower()}"
|
||||
if dst.exists():
|
||||
continue
|
||||
try:
|
||||
os.link(f, dst)
|
||||
except OSError:
|
||||
shutil.copy2(f, dst)
|
||||
print(f"[organize] {dst.relative_to(SHOWS_ROOT)}", flush=True)
|
||||
|
||||
def cmd_organize(a):
|
||||
organize(a.stage, a.name, a.year, a.season)
|
||||
|
||||
def cmd_download(a):
|
||||
torrent = Path(a.torrent).expanduser().resolve()
|
||||
if not torrent.exists():
|
||||
sys.exit(f"not found: {torrent}")
|
||||
stage = DOWNLOADS / torrent.stem
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
hook = stage / "_hook.sh"
|
||||
hook.write_text(
|
||||
f'#!/bin/bash\n'
|
||||
f'python3 "{Path(__file__).resolve()}" organize '
|
||||
f'--stage "{stage}" --name "{a.name}" --year "{a.year}" --season {a.season}\n'
|
||||
)
|
||||
hook.chmod(0o755)
|
||||
cmd = [
|
||||
"aria2c", str(torrent),
|
||||
f"--dir={stage}",
|
||||
f"--listen-port={PORT}", f"--dht-listen-port={PORT}",
|
||||
"--enable-dht=true", "--enable-peer-exchange=true", "--bt-enable-lpd=true",
|
||||
"--follow-torrent=mem", "--check-integrity=true",
|
||||
"--summary-interval=30", "--console-log-level=warn",
|
||||
f"--on-bt-download-complete={hook}",
|
||||
]
|
||||
if a.no_seed:
|
||||
cmd.append("--seed-time=0")
|
||||
else:
|
||||
cmd += [f"--seed-ratio={SEED_RATIO}", f"--seed-time={SEED_TIME_MIN}"]
|
||||
subprocess.run(cmd)
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
d = sub.add_parser("download")
|
||||
d.add_argument("torrent")
|
||||
d.add_argument("--name", required=True)
|
||||
d.add_argument("--year", required=True)
|
||||
d.add_argument("--season", type=int, default=1)
|
||||
d.add_argument("--no-seed", action="store_true")
|
||||
d.set_defaults(func=cmd_download)
|
||||
o = sub.add_parser("organize")
|
||||
o.add_argument("--stage", required=True)
|
||||
o.add_argument("--name", required=True)
|
||||
o.add_argument("--year", required=True)
|
||||
o.add_argument("--season", type=int, default=1)
|
||||
o.set_defaults(func=cmd_organize)
|
||||
a = ap.parse_args()
|
||||
a.func(a)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
nyaa-tools-old/README.md
Normal file
13
nyaa-tools-old/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# [nyaa-tools](https://sr.ht/~truita/nyaa-tools)
|
||||
`nyaa-tools` is a set of scripts for automating downloads from [Nyaa.si](https://nyaa.si).
|
||||
|
||||
It is currently comprised of:
|
||||
- `fetch`: intended to be run as a cron job, will download files through a
|
||||
torrent daemon and link them in a [Jellyfin style](https://jellyfin.org/docs/general/server/media/shows) organization
|
||||
- `helper`: provides a CLI for adding items to the tracking list which `fetch` employs
|
||||
|
||||
### Dependencies
|
||||
- toml
|
||||
- requests
|
||||
- transmission-rpc (fetch only)
|
||||
- inquirer (helper only)
|
||||
91
nyaa-tools-old/batch.py
Normal file
91
nyaa-tools-old/batch.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
from time import sleep
|
||||
|
||||
import toml
|
||||
import transmission_rpc
|
||||
|
||||
# https://franklingu.github.io/programming/2016/03/01/creating-daemon-process-python-example-explanation/
|
||||
def daemonize():
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# exit first parent
|
||||
exit(0)
|
||||
except OSError:
|
||||
exit(1)
|
||||
|
||||
# decouple from parent environment
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
|
||||
# do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
exit(0)
|
||||
except OSError:
|
||||
exit(1)
|
||||
|
||||
config = toml.load("/etc/nyaa-tools/config.toml")
|
||||
|
||||
def jelly_path(name: str, season: int, episode=None, offset=0) -> str:
|
||||
if episode is None:
|
||||
return "{base}/{name}/Season {season:02}".format(
|
||||
base=config["library"]["base_path"],
|
||||
name=name,
|
||||
season=season,
|
||||
)
|
||||
else:
|
||||
return "{base}/{name}/Season {season:02}/Episode S{season:02}E{episode:02}.mkv".format(
|
||||
base=config["library"]["base_path"],
|
||||
name=name,
|
||||
season=season,
|
||||
episode=episode + offset,
|
||||
)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="nyaa-batch"
|
||||
)
|
||||
parser.add_argument('-t', '--title')
|
||||
parser.add_argument('-s', '--season')
|
||||
parser.add_argument('-l', '--link')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
season = int(args.season)
|
||||
except TypeError:
|
||||
exit(1)
|
||||
|
||||
|
||||
client = transmission_rpc.Client(host=config["transmission"]["host"])
|
||||
torrent = client.add_torrent(args.link, files_wanted=None, paused=True)
|
||||
torrent = client.get_torrent(torrent.id) # Don't ask
|
||||
files = torrent.get_files()
|
||||
files.sort()
|
||||
wanted = []
|
||||
for file in files:
|
||||
# Only get files in root
|
||||
if file.name.count('/') < 2:
|
||||
wanted.append((file.id, len(wanted) + 1))
|
||||
|
||||
client.change_torrent(torrent.id, files_wanted=map(lambda f: f[0], wanted))
|
||||
client.start_torrent(torrent.id)
|
||||
daemonize()
|
||||
|
||||
while len(wanted) > 0:
|
||||
torrent = client.get_torrent(torrent.id)
|
||||
get_files = torrent.get_files()
|
||||
for want in wanted:
|
||||
file = next(x for x in get_files if x.id == want[0])
|
||||
if file.size == file.completed:
|
||||
if not os.path.exists(jelly_path(args.title, season)):
|
||||
os.makedirs(jelly_path(args.title, season))
|
||||
os.link(
|
||||
config["transmission"]["download_path"] + "/" + file.name,
|
||||
jelly_path(args.title, season, want[1])
|
||||
)
|
||||
wanted.remove(want)
|
||||
sleep(5)
|
||||
126
nyaa-tools-old/fetch.py
Executable file
126
nyaa-tools-old/fetch.py
Executable file
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python3
|
||||
from time import sleep
|
||||
import requests
|
||||
import toml
|
||||
import logging
|
||||
import xml.dom.minidom
|
||||
import re
|
||||
import transmission_rpc
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
logging.basicConfig(format="%(name)s - %(levelname)s - %(message)s", level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
config = toml.load("/etc/nyaa-tools/config.toml")
|
||||
series = toml.load("/etc/nyaa-tools/series.toml")
|
||||
release_groups = toml.load("/etc/nyaa-tools/release-groups.toml")
|
||||
|
||||
|
||||
def jelly_path(name: str, season: int, episode=None, offset=0) -> str:
|
||||
if episode is None:
|
||||
return "{base}/{name}/Season {season:02}".format(
|
||||
base=config["library"]["base_path"],
|
||||
name=name,
|
||||
season=season,
|
||||
)
|
||||
else:
|
||||
return "{base}/{name}/Season {season:02}/Episode S{season:02}E{episode:02}.mkv".format(
|
||||
base=config["library"]["base_path"],
|
||||
name=name,
|
||||
season=season,
|
||||
episode=episode + offset,
|
||||
)
|
||||
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
logger.info("Searching for {}".format(" ".join(sys.argv[1:])))
|
||||
nyaa_body = requests.get(
|
||||
"https://nyaa.si/?page=rss&q={}".format(" ".join(sys.argv[1:]))
|
||||
).text
|
||||
else:
|
||||
nyaa_body = requests.get("https://nyaa.si/?page=rss").text
|
||||
items = xml.dom.minidom.parseString(nyaa_body).getElementsByTagName("item")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadItem:
|
||||
torrent_id: int
|
||||
name: str
|
||||
season: int
|
||||
episode: int
|
||||
offset: int
|
||||
|
||||
|
||||
downloading: list[DownloadItem] = []
|
||||
client: transmission_rpc.Client = None
|
||||
|
||||
|
||||
for item in items:
|
||||
title: str = item.getElementsByTagName("title")[0].firstChild.data
|
||||
link: str = item.getElementsByTagName("link")[0].firstChild.data
|
||||
|
||||
logger.debug("Checking {}".format(title))
|
||||
|
||||
for season in series["tracking"]:
|
||||
regex = re.search(release_groups[season["release_group"]], title)
|
||||
if regex is None:
|
||||
continue
|
||||
if season["name"] in regex.group("title"):
|
||||
logger.debug("Title matched for {}".format(season["name"]))
|
||||
# Extract chapter number
|
||||
chapter = int(regex.group("episode"))
|
||||
logger.debug("Chapter extracted: {}".format(chapter))
|
||||
|
||||
# Check if chapter already exists
|
||||
path = jelly_path(
|
||||
season["name"], season["season"], chapter, season.get("offset", 0)
|
||||
)
|
||||
if os.path.exists(path):
|
||||
logger.debug(
|
||||
'Chapter {} for "{}" already exists. Skipping'.format(
|
||||
chapter, season["name"]
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Sign in to Transmission
|
||||
client = transmission_rpc.Client(host=config["transmission"]["host"])
|
||||
torrent = client.add_torrent(link)
|
||||
downloading.append(
|
||||
DownloadItem(
|
||||
torrent.id,
|
||||
season["name"],
|
||||
season["season"],
|
||||
chapter,
|
||||
season.get("offset", 0),
|
||||
)
|
||||
)
|
||||
logger.info("Downloading {}".format(title))
|
||||
|
||||
while len(downloading) > 0:
|
||||
for item in downloading:
|
||||
torrent = client.get_torrent(item.torrent_id)
|
||||
if torrent.left_until_done == 0:
|
||||
if not os.path.exists(jelly_path(item.name, item.season)):
|
||||
logger.warning(
|
||||
'Season folder for "{}" did not exist, creating recursively'.format(
|
||||
item.name
|
||||
)
|
||||
)
|
||||
os.makedirs(jelly_path(item.name, item.season))
|
||||
|
||||
os.link(
|
||||
config["transmission"]["download_path"]
|
||||
+ "/"
|
||||
+ torrent.get_files()[0].name,
|
||||
jelly_path(item.name, item.season, item.episode, item.offset),
|
||||
)
|
||||
logger.info(
|
||||
'Linked "{}" season {} chapter {}'.format(
|
||||
item.name, item.season, item.episode + item.offset
|
||||
)
|
||||
)
|
||||
downloading.remove(item)
|
||||
sleep(5)
|
||||
124
nyaa-tools-old/helper.py
Executable file
124
nyaa-tools-old/helper.py
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import toml
|
||||
import xml.dom.minidom
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
import inquirer
|
||||
|
||||
release_groups = toml.load("/etc/nyaa-tools/release-groups.toml")
|
||||
release_group_names = release_groups.keys()
|
||||
|
||||
query = inquirer.text("Name of the anime")
|
||||
nyaa_search = requests.get("https://nyaa.si/?page=rss&q={}".format(query)).text
|
||||
items = xml.dom.minidom.parseString(nyaa_search).getElementsByTagName("item")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Episode:
|
||||
title: str
|
||||
link: str
|
||||
release_group: str
|
||||
|
||||
|
||||
def episode_info(ep: Episode, season: int, offset: int) -> str:
|
||||
return (
|
||||
"Title: {}\n".format(ep.title)
|
||||
+ "Season: {}\n".format(season)
|
||||
+ "Chapter offset: {}\n".format(offset)
|
||||
+ "Release Group: {}".format(ep.release_group)
|
||||
)
|
||||
|
||||
|
||||
matches: list[Episode] = []
|
||||
|
||||
for item in items:
|
||||
title: str = item.getElementsByTagName("title")[0].firstChild.data
|
||||
link: str = item.getElementsByTagName("link")[0].firstChild.data
|
||||
|
||||
regex = re.search("\\[([^ ]+)\\].+", title)
|
||||
if regex is not None:
|
||||
matches.append(Episode(title, link, regex.group(1)))
|
||||
|
||||
if not matches:
|
||||
print("No matching entries")
|
||||
exit(255)
|
||||
|
||||
known_matches = filter(lambda ep: ep.release_group in release_group_names, matches)
|
||||
parsed_matches: list[Episode] = []
|
||||
|
||||
for match in known_matches:
|
||||
rg_regex = re.search(release_groups[match.release_group], match.title)
|
||||
if rg_regex is not None:
|
||||
title = rg_regex.group(1)
|
||||
parsed_matches.append(Episode(title, match.link, match.release_group))
|
||||
|
||||
if not parsed_matches:
|
||||
print("No known release groups were found, however the following were identified:")
|
||||
for match in set(map(lambda m: m.release_group, matches)):
|
||||
print("-", match)
|
||||
print(
|
||||
"Please add one of them to the release-groups.toml or wait for a release from a known group"
|
||||
)
|
||||
exit(255)
|
||||
|
||||
choices = set(map(lambda m: m.release_group, parsed_matches))
|
||||
if len(choices) > 1:
|
||||
chosen_rg = inquirer.list_input(
|
||||
"Choose a release group",
|
||||
choices=sorted(set(map(lambda m: m.release_group, parsed_matches))),
|
||||
)
|
||||
else:
|
||||
chosen_rg = choices.pop()
|
||||
|
||||
final_match = next(m for m in parsed_matches if m.release_group == chosen_rg)
|
||||
season = 1
|
||||
offset = 0
|
||||
while True:
|
||||
print(episode_info(final_match, season, offset))
|
||||
ans = inquirer.list_input(
|
||||
"The following entry will be added",
|
||||
choices=["Confirm", "Edit", "Cancel"],
|
||||
)
|
||||
if ans == "Confirm":
|
||||
break
|
||||
elif ans == "Cancel":
|
||||
exit(0)
|
||||
while True:
|
||||
print(episode_info(final_match, season, offset))
|
||||
ans = inquirer.list_input(
|
||||
"What do you want to edit", choices=["Title", "Season", "Offset", "Done"]
|
||||
)
|
||||
if ans == "Done":
|
||||
break
|
||||
if ans == "Title":
|
||||
final_match.title = inquirer.text("New title", default=final_match.title)
|
||||
elif ans == "Season":
|
||||
season = int(inquirer.text(
|
||||
"Season number", validate=lambda _, x: re.match("\\d+", x)
|
||||
))
|
||||
elif ans == "Offset":
|
||||
offset = int(inquirer.text(
|
||||
"Chapter offset", validate=lambda _, x: re.match("\\d+", x)
|
||||
))
|
||||
|
||||
series = toml.load("/etc/nyaa-tools/series.toml")
|
||||
if "tracking" not in series.keys():
|
||||
series["tracking"] = []
|
||||
|
||||
entry = {
|
||||
"name": final_match.title,
|
||||
"season": season,
|
||||
"release_group": final_match.release_group,
|
||||
}
|
||||
if offset != 0:
|
||||
entry["offset"] = offset
|
||||
series["tracking"].append(entry)
|
||||
|
||||
with open("/etc/nyaa-tools/series.toml", "w") as f:
|
||||
toml.dump(series, f)
|
||||
print(
|
||||
"Done! Please run `nyaa-fetch '[{}] {}'` to download previous episodes".format(
|
||||
final_match.release_group, final_match.title
|
||||
)
|
||||
)
|
||||
253
nyaa-tools-old/web/server.py
Normal file
253
nyaa-tools-old/web/server.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
#!/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import xml.dom.minidom
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
import toml
|
||||
from flask import (
|
||||
Flask,
|
||||
make_response,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
release_groups = toml.load("/etc/nyaa-tools/release-groups.toml")
|
||||
batch_groups = toml.load("/etc/nyaa-tools/batch-groups.toml")
|
||||
|
||||
DEV_ENV = bool(os.environ.get("DEV_ENV", default=False))
|
||||
if not DEV_ENV:
|
||||
PASSWORD = os.environ["PASSWORD"]
|
||||
|
||||
|
||||
active_sessions = {}
|
||||
|
||||
|
||||
def is_token_valid(token):
|
||||
return (
|
||||
token and token in active_sessions and datetime.now() < active_sessions[token]
|
||||
)
|
||||
|
||||
|
||||
# Creates a token and adds it to the "database"
|
||||
def create_valid_token():
|
||||
token = secrets.token_urlsafe(16)
|
||||
|
||||
current_time = datetime.now()
|
||||
expiration_time = current_time + timedelta(days=1)
|
||||
|
||||
active_sessions[token] = expiration_time
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def remove_token(token):
|
||||
del active_sessions[token]
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
if not DEV_ENV:
|
||||
if request.path == "/login":
|
||||
return login()
|
||||
else:
|
||||
if not request.path.startswith("/static"):
|
||||
token = request.cookies.get("session_token")
|
||||
if not is_token_valid(token):
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchItem:
|
||||
title: str
|
||||
link: str
|
||||
|
||||
def search(query) -> list[SearchItem]:
|
||||
nyaa_search = requests.get("https://nyaa.si/?page=rss&q={}".format(query)).text
|
||||
items = xml.dom.minidom.parseString(nyaa_search).getElementsByTagName("item")
|
||||
|
||||
result = []
|
||||
|
||||
for item in items:
|
||||
title: str = item.getElementsByTagName("title")[0].firstChild.data # pyright: ignore[reportOptionalMemberAccess,reportAttributeAccessIssue]
|
||||
link: str = item.getElementsByTagName("link")[0].firstChild.data # pyright: ignore[reportOptionalMemberAccess,reportAttributeAccessIssue]
|
||||
|
||||
result.append(SearchItem(title, link))
|
||||
return result
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
password = request.form["password"]
|
||||
|
||||
if password == PASSWORD:
|
||||
token = create_valid_token()
|
||||
|
||||
# Set token in a cookie
|
||||
resp = make_response(redirect(url_for("index")))
|
||||
resp.set_cookie("session_token", token, max_age=60 * 60 * 24)
|
||||
|
||||
return resp
|
||||
else:
|
||||
return "Invalid credentials. Try again."
|
||||
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
# Get the session token
|
||||
token = request.cookies.get("session_token")
|
||||
|
||||
remove_token(token)
|
||||
|
||||
# Clear the session and cookie
|
||||
resp = make_response(redirect(url_for("login")))
|
||||
resp.set_cookie("session_token", "", expires=0) # Remove cookie
|
||||
return resp
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeriesInfo:
|
||||
release_group: str
|
||||
title: str
|
||||
season: int | None
|
||||
|
||||
|
||||
# Returns the search page if q is empty, else it shows results
|
||||
@app.route("/series", methods=["GET", "POST"])
|
||||
def series():
|
||||
if request.method == "GET":
|
||||
query = request.args.get("q", default="", type=str)
|
||||
|
||||
if query == "":
|
||||
return render_template("series/search.html")
|
||||
|
||||
results = search(query)
|
||||
parsed_results = []
|
||||
for item in results:
|
||||
title = item.title
|
||||
rg = re.search("\\[([^ ]+)\\].+", title)
|
||||
if rg is None or rg.group(1) not in release_groups.keys():
|
||||
continue
|
||||
info = re.search(release_groups[rg.group(1)], title)
|
||||
# TODO: Do not append duplicated entries
|
||||
if info is None:
|
||||
continue
|
||||
try:
|
||||
season = int(info.group("season"))
|
||||
except IndexError:
|
||||
season = None
|
||||
parsed_results.append(
|
||||
SeriesInfo(
|
||||
release_group=rg.group(1),
|
||||
title=info.group("title"),
|
||||
season=season
|
||||
)
|
||||
)
|
||||
return render_template("series/results.html", results=parsed_results)
|
||||
elif request.method == "POST":
|
||||
release_group = request.form["release-group"]
|
||||
title = request.form["title"]
|
||||
season = request.form["season"]
|
||||
if release_group and title and season:
|
||||
try:
|
||||
season = int(season)
|
||||
except ValueError:
|
||||
return "Invalid season", 400
|
||||
|
||||
series = toml.load("/etc/nyaa-tools/series.toml")
|
||||
if "tracking" not in series.keys():
|
||||
series["tracking"] = []
|
||||
|
||||
# TODO: Do not allow duplicates
|
||||
entry = {"name": title, "release_group": release_group, "season": season}
|
||||
series["tracking"].append(entry)
|
||||
with open("/etc/nyaa-tools/series.toml", "w") as f:
|
||||
toml.dump(series, f)
|
||||
return "OK", 200
|
||||
else:
|
||||
return "Missing parameters", 400
|
||||
|
||||
|
||||
@dataclass
|
||||
class BatchInfo:
|
||||
release_group: str
|
||||
title: str
|
||||
season: int | None
|
||||
episodes: int | None
|
||||
link: str
|
||||
|
||||
@app.route("/batch", methods=["GET", "POST"])
|
||||
def batch():
|
||||
if request.method == "GET":
|
||||
query = request.args.get("q", default="", type=str)
|
||||
|
||||
if query == "":
|
||||
return render_template("batch/search.html")
|
||||
|
||||
results = search(query)
|
||||
parsed_results = []
|
||||
for item in results:
|
||||
title = item.title
|
||||
rg = re.search("\\[([^ ]+)\\].+", title)
|
||||
if rg is None or rg.group(1) not in batch_groups.keys():
|
||||
continue
|
||||
info = re.search(batch_groups[rg.group(1)], title)
|
||||
# TODO: Do not append duplicated entries
|
||||
if info is None:
|
||||
continue
|
||||
try:
|
||||
season = int(info.group("season"))
|
||||
except IndexError:
|
||||
season = None
|
||||
|
||||
try:
|
||||
episodes = int(info.group("last")) - int(info.group("first")) + 1
|
||||
except IndexError:
|
||||
#TODO: Get episodes from file list (requires lxml & cssselector)
|
||||
episodes = None
|
||||
|
||||
parsed_results.append(
|
||||
BatchInfo(
|
||||
release_group=rg.group(1),
|
||||
title=info.group("title"),
|
||||
season=season,
|
||||
episodes=episodes,
|
||||
link=item.link
|
||||
)
|
||||
)
|
||||
return render_template("batch/results.html", results=parsed_results)
|
||||
elif request.method == "POST":
|
||||
title = request.form["title"]
|
||||
season = request.form["season"]
|
||||
link = request.form["link"]
|
||||
if title and season and link:
|
||||
try:
|
||||
season = int(season)
|
||||
except ValueError:
|
||||
return "Invalid season", 400
|
||||
|
||||
ret = os.system(" ".join(["nyaa-batch", "-t", "'{}'".format(title), "-s", str(season), "-l", "'{}'".format(link)]))
|
||||
if ret == 0:
|
||||
return "OK", 200
|
||||
else:
|
||||
return "Error", 500
|
||||
else:
|
||||
return "Missing parameters", 400
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=DEV_ENV)
|
||||
110
nyaa-tools-old/web/static/style.css
Normal file
110
nyaa-tools-old/web/static/style.css
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #1e1e1e;
|
||||
color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: #2b2b2b;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.full_form {
|
||||
background-color: #2b2b2b;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button_form {
|
||||
margin-left: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
background-color: #333;
|
||||
border: 1px solid #555;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #359039;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
color: #f5f5f5;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #f5f5f5;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 60vw;
|
||||
}
|
||||
|
||||
li {
|
||||
background-color: #2b2b2b;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
transition: background-color 0.3s ease;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
28
nyaa-tools-old/web/templates/batch/results.html
Normal file
28
nyaa-tools-old/web/templates/batch/results.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% import 'macros.html' as macros %}
|
||||
{{ macros.header('Search') }}
|
||||
|
||||
<body>
|
||||
<h1>Search Results</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Release group</th>
|
||||
<th>Name</th>
|
||||
<th>Season</th>
|
||||
<th>Episodes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for item in results %}
|
||||
<tr><form action="/batch" method="post">
|
||||
<td>{{ item.release_group }}</td>
|
||||
<td><input type="text" name="title" value="{{ item.title }}" required /></td>
|
||||
<td><input type="number" name="season" {% if item.season %}value="{{ item.season }}"{% endif %} min="1" max="99" required /></td>
|
||||
<td>{% if item.episodes %}{{item.episodes}}{% else %}?{% endif %}</td>
|
||||
<input type="hidden" name="link" value="{{ item.link }}">
|
||||
<td><input type="submit" value="Download" class="btn-link" /></td>
|
||||
</form></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a href="/">Home</a>
|
||||
</body>
|
||||
|
||||
{{ macros.footer() }}
|
||||
6
nyaa-tools-old/web/templates/batch/search.html
Normal file
6
nyaa-tools-old/web/templates/batch/search.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{% import 'macros.html' as macros %}
|
||||
{{ macros.header('Search') }}
|
||||
|
||||
{{ macros.search('batch', "add 'batch' to the search terms to find batches") }}
|
||||
|
||||
{{ macros.footer() }}
|
||||
12
nyaa-tools-old/web/templates/index.html
Normal file
12
nyaa-tools-old/web/templates/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{% import 'macros.html' as macros %}
|
||||
{{ macros.header('Hub')}}
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/series">Add series</a>
|
||||
<a href="/batch">Download batch</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{{ macros.footer() }}
|
||||
13
nyaa-tools-old/web/templates/login.html
Normal file
13
nyaa-tools-old/web/templates/login.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% import 'macros.html' as macros %}
|
||||
{{ macros.header('Login')}}
|
||||
|
||||
<body>
|
||||
<h1>Login</h1>
|
||||
<form class="full_form" method="POST">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required><br><br>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
{{ macros.footer() }}
|
||||
29
nyaa-tools-old/web/templates/macros.html
Normal file
29
nyaa-tools-old/web/templates/macros.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{% macro header(title) %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="/static/full_page.css">
|
||||
</head>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro footer() %}
|
||||
</html>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro search(path, tip) %}
|
||||
<body>
|
||||
<h1>Search</h1>
|
||||
<form class="full_form" action="/{{ path }}" method="GET">
|
||||
<label for="query">Search for:</label>
|
||||
<input type="text" id="query" name="q" required /><br>
|
||||
{%- if tip %}<i>Tip: {{ tip }}</i><br>{% endif -%}
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<a href="/">Home</a>
|
||||
</body>
|
||||
{% endmacro %}
|
||||
25
nyaa-tools-old/web/templates/series/results.html
Normal file
25
nyaa-tools-old/web/templates/series/results.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% import 'macros.html' as macros %}
|
||||
{{ macros.header('Search') }}
|
||||
|
||||
<body>
|
||||
<h1>Search Results</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Release group</th>
|
||||
<th>Name</th>
|
||||
<th>Season</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for item in results %}
|
||||
<tr><form action="/series" method="post">
|
||||
<td>{{item.release_group}}<input type="hidden" name="release-group" value="{{item.release_group}}" readonly required/></td>
|
||||
<td><input type="text" name="title" value="{{item.title}}" required /></td>
|
||||
<td><input type="number" name="season" {% if item.season %}value="{{item.season}}"{% endif %} min="1" max="99" required /></td>
|
||||
<td><input type="submit" value="Add" class="btn-link" /></td>
|
||||
</form></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a href="/">Home</a>
|
||||
</body>
|
||||
|
||||
{{ macros.footer() }}
|
||||
6
nyaa-tools-old/web/templates/series/search.html
Normal file
6
nyaa-tools-old/web/templates/series/search.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{% import 'macros.html' as macros %}
|
||||
{{ macros.header('Search') }}
|
||||
|
||||
{{ macros.search('series', "if a series' season is not autodetected you may need to remove it from the title") }}
|
||||
|
||||
{{ macros.footer() }}
|
||||
Loading…
Add table
Reference in a new issue