added anime namer
This commit is contained in:
parent
e5acec3513
commit
7fbac97e77
8
anime-namer/Dockerfile
Normal file
8
anime-namer/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM python:3.7
|
||||
|
||||
RUN pip3 install flask requests
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
CMD python3 app.py
|
95
anime-namer/app.py
Normal file
95
anime-namer/app.py
Normal file
@ -0,0 +1,95 @@
|
||||
from flask import Flask, render_template, request, redirect, jsonify, make_response
|
||||
from os import listdir, getenv, path
|
||||
from requests import post
|
||||
from random import randint
|
||||
|
||||
JELLYFIN_DIRECTORY = getenv("JF_DIR")
|
||||
FLAG_DIRECTORY = getenv("FLAG_DIR")
|
||||
GOTIFY_URL = getenv("GOTIFY_URL")
|
||||
BASE_URL = getenv("BASE_URL")
|
||||
|
||||
app = Flask(__name__)
|
||||
pending = {}
|
||||
|
||||
class PendingNaming:
|
||||
def __init__(self, dl_path):
|
||||
self.dl_path = dl_path
|
||||
self.id = str(randint(100000, 999999))
|
||||
|
||||
def resolve(self):
|
||||
pending.pop(self.id, None)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", pending=[pending[i] for i in pending], BASE_URL=BASE_URL)
|
||||
|
||||
@app.route("/<int:id>")
|
||||
def index_with_id(id):
|
||||
id = str(id)
|
||||
if id in pending:
|
||||
item = pending[id]
|
||||
return render_template("resolve.html", item=item, BASE_URL=BASE_URL)
|
||||
else:
|
||||
return make_response("Pending item not found", 404)
|
||||
|
||||
@app.route("/addPending", methods=["POST", "GET"])
|
||||
def add_pending():
|
||||
dl_path = request.args.get("title")
|
||||
if not dl_path:
|
||||
dl_path = request.form.get("title")
|
||||
if not dl_path:
|
||||
return make_response("Title not provided", 400)
|
||||
p = PendingNaming(dl_path)
|
||||
pending[p.id] = p
|
||||
|
||||
url = "{}/{}".format(BASE_URL, p.id)
|
||||
post(GOTIFY_URL, json={
|
||||
"extras": {
|
||||
"client::display": {
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
"client::android": {
|
||||
"autoLink": "web"
|
||||
}
|
||||
},
|
||||
"title": "Intervention needed",
|
||||
"message": "[{} could not be named]({})".format(dl_path, url),
|
||||
"priority": 9
|
||||
})
|
||||
return str(p.id)
|
||||
|
||||
@app.route("/resolve", methods=["POST"])
|
||||
def resolve():
|
||||
id = request.form.get("id")
|
||||
title = request.form.get("title")
|
||||
season = request.form.get("season")
|
||||
if not id in pending:
|
||||
return make_response("Pending item not found", 404)
|
||||
item = pending[id]
|
||||
with open(path.join(FLAG_DIRECTORY, id), "w+") as f:
|
||||
f.write("{0}|{0}|{1}".format(title, season))
|
||||
item.resolve()
|
||||
return redirect(BASE_URL)
|
||||
|
||||
@app.route("/delete/<int:id>", methods=["GET"])
|
||||
def delete(id):
|
||||
id = str(id)
|
||||
if not id in pending:
|
||||
return make_response("Pending item not found", 404)
|
||||
item = pending[id]
|
||||
with open(path.join(FLAG_DIRECTORY, id), "w+") as f:
|
||||
f.write("delete")
|
||||
item.resolve()
|
||||
return redirect(BASE_URL)
|
||||
|
||||
@app.route("/autocomplete")
|
||||
def autocomplete():
|
||||
dirs = listdir(JELLYFIN_DIRECTORY)
|
||||
query = request.args.get("term").lower()
|
||||
if len(query) >= 2:
|
||||
return jsonify([i for i in dirs if query in i.lower()])
|
||||
return jsonify([])
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
app.run("0.0.0.0", getenv("PORT"))
|
22
anime-namer/docker-compose.yml
Normal file
22
anime-namer/docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
version: '3'
|
||||
services:
|
||||
anime-namer:
|
||||
build: .
|
||||
container_name: anime-namer
|
||||
networks:
|
||||
- anamer-network
|
||||
ports:
|
||||
- 9010:9010
|
||||
environment:
|
||||
- JF_DIR=THE_JELLYFIN_DIRECTORY
|
||||
- FLAG_DIR=THE_FLAG_DIRECTORY
|
||||
- BASE_URL=YOUR_BASE_URL
|
||||
- GOTIFY_URL=YOUR_GOTIFY_URL
|
||||
- PORT=9010
|
||||
volumes:
|
||||
- /mnt/Storage/Anime:/Anime
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
anamer-network:
|
||||
driver: bridge
|
83
anime-namer/templates/index.html
Normal file
83
anime-namer/templates/index.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Anime namer</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul li {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 12px 8px 12px 40px;
|
||||
background: #eee;
|
||||
font-size: 18px;
|
||||
transition: 0.2s;
|
||||
|
||||
/* make the list items unselectable */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
ul li:nth-child(odd) {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
ul li:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
ul li.checked {
|
||||
background: #888;
|
||||
color: #fff;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
ul li.checked::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-color: #fff;
|
||||
border-style: solid;
|
||||
border-width: 0 2px 2px 0;
|
||||
top: 10px;
|
||||
left: 16px;
|
||||
transform: rotate(45deg);
|
||||
height: 15px;
|
||||
width: 7px;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 12px 16px 12px 16px;
|
||||
z-index: 99;
|
||||
}
|
||||
.close:hover {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Anime namer</h1>
|
||||
<div>
|
||||
<h2>Pending:</h2>
|
||||
<ul>
|
||||
{% for i in pending %}
|
||||
<li>
|
||||
<a href="{{ BASE_URL }}/{{ i.id }}">{{ i.dl_path }}</a>
|
||||
<a href="{{ BASE_URL }}/delete/{{ i.id }}"><span class="close">X</span></a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
73
anime-namer/templates/resolve.html
Normal file
73
anime-namer/templates/resolve.html
Normal file
@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
||||
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||
<title>Anime namer</title>
|
||||
<style>
|
||||
input[type=text],
|
||||
input[type=number],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 16px;
|
||||
resize: vertical
|
||||
}
|
||||
.button,
|
||||
input[type=submit] {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.button:hover,
|
||||
input[type=submit]:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.container {
|
||||
border-radius: 5px;
|
||||
background-color: #f2f2f2;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>{{ item.dl_path }}</h2>
|
||||
<form method="POST" action="{{ BASE_URL }}/resolve">
|
||||
<input style="display:none;" type="text" name="id" value="{{ item.id }}">
|
||||
<label for="titleInput">Title</label>
|
||||
<input type="text" name="title" id="titleInput">
|
||||
<label for="titleInput">Season</label>
|
||||
<input type="number" name="season">
|
||||
<input type="submit">
|
||||
<a href="{{ BASE_URL }}/delete/{{ item.id }}"><button class="button" style="background-color: red;">Delete</button></a>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
$(function () {
|
||||
$("#titleInput").autocomplete({
|
||||
source: "{{ BASE_URL }}/autocomplete",
|
||||
minLength: 2,
|
||||
select: function (event, ui) {
|
||||
$("#titleInput").val(ui.item.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
74
anime_scripts/jellyfin-namer-new.sh
Executable file
74
anime_scripts/jellyfin-namer-new.sh
Executable file
@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
|
||||
#Gets the last part of the current directory
|
||||
function get_bottom_dir() {
|
||||
IFS='/';
|
||||
read -ra ADDR <<< "$PWD";
|
||||
echo "${ADDR[-1]}";
|
||||
IFS=' ';
|
||||
}
|
||||
|
||||
#Removes braces, parenteres along with everything in them and strips leading and trailing whitespace
|
||||
function name_clean() {
|
||||
local _out=$(echo "$1" | sed -e 's/\[[^][]*\]//g');
|
||||
_out=$(echo "$_out" | sed -e 's/([^()]*)//g');
|
||||
_out=$(echo "$_out" | sed 's/_/ /g');
|
||||
echo $(echo "$_out" | xargs);
|
||||
}
|
||||
|
||||
#Get series via seasons.py. Will fall-back to manual intervention if fail.
|
||||
function get_series() {
|
||||
local sanitized_name=$(name_clean "$1");
|
||||
local output;
|
||||
output=$(python3 /scripts/seasons.py "$sanitized_name"; exit "$?";);
|
||||
if [[ "$?" -ne "0" ]]; then
|
||||
echo "seasons.py failed. Waiting for manual intevention.";
|
||||
local mi_id;
|
||||
mi_id=$(curl --fail -F "dl_path=$sanitized_name" "$MI_URL"; exit "$?";);
|
||||
if [[ "$?" -eq "0" ]]; then
|
||||
while [[ ! -f "/Anime/flags/$mi_id" ]]; do
|
||||
sleep 1;
|
||||
done
|
||||
output=$(cat "/Anime/flags/$mi_id");
|
||||
rm "/Anime/flags/$mi_id";
|
||||
if [[ "$output" -eq "delete" ]]; then
|
||||
return 1;
|
||||
fi
|
||||
else
|
||||
return 1;
|
||||
fi
|
||||
fi
|
||||
|
||||
IFS="|";
|
||||
read -ra STR <<< "$output"
|
||||
|
||||
TITLE_ROMAJI="${STR[0]}";
|
||||
TITLE_ENGLISH="${STR[1]}";
|
||||
SEASON="${STR[2]}";
|
||||
IFS=" ";
|
||||
return 0;
|
||||
}
|
||||
cd "$1";
|
||||
|
||||
bottom_dir=$(get_bottom_dir);
|
||||
cleaned_bottom_dir=$(name_clean "$bottom_dir");
|
||||
get_series "$cleaned_bottom_dir";
|
||||
if [[ "$?" -eq "0" ]]; then
|
||||
if [[ -d "$JF_DIR/$TITLE_ROMAJI" ]]; then
|
||||
cleaned_dir="$TITLE_ROMAJI/Season $SEASON";
|
||||
elif [[ -d "$JF_DIR/$TITLE_ENGLISH" ]]; then
|
||||
cleaned_dir="$TITLE_ENGLISH/Season $SEASON";
|
||||
else
|
||||
cleaned_dir="$TITLE_ROMAJI/Season $SEASON";
|
||||
fi
|
||||
else
|
||||
cleaned_dir="$cleaned_bottom_dir";
|
||||
fi
|
||||
mkdir -p "$JF_DIR/$cleaned_dir";
|
||||
|
||||
for i in *; do
|
||||
cleaned_name=$(name_clean "$i");
|
||||
ln "$PWD/$i" "$JF_DIR/$cleaned_dir/$cleaned_name" >/dev/null 2>/dev/null;
|
||||
done;
|
||||
|
||||
echo "$JF_DIR/$cleaned_dir";
|
150
anime_scripts/seasons.py
Executable file
150
anime_scripts/seasons.py
Executable file
@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from requests import post
|
||||
from datetime import datetime
|
||||
from sys import argv
|
||||
|
||||
BASE_URL = "https://graphql.anilist.co"
|
||||
|
||||
class BaseShowNotInList(Exception):
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
super().__init__("Base show not in sequel/prequel list.")
|
||||
|
||||
class SortableAnime:
|
||||
def __init__(self, id, year, month, day, reltype, title, frmt):
|
||||
self.id = id
|
||||
self.timestamp = datetime(year if year else 9999, month if month else 12, day if day else 31)
|
||||
self.type = reltype
|
||||
self.frmt = frmt
|
||||
self.title = title
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"timestamp": self.timestamp.strftime("%d-%m-%Y"),
|
||||
"type": self.type,
|
||||
"title": self.title,
|
||||
"format": self.frmt
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
def __hash__(self):
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return str(self.title)
|
||||
|
||||
|
||||
def search(query):
|
||||
response = post(BASE_URL, json={
|
||||
'query': """
|
||||
query ($q: String) {
|
||||
Media (search: $q) {
|
||||
id
|
||||
}
|
||||
}
|
||||
""",
|
||||
'variables': {"q": query}
|
||||
})
|
||||
if response.ok:
|
||||
return response.json()["data"]["Media"]["id"]
|
||||
raise ValueError("Show does not exist")
|
||||
|
||||
def get_show(id):
|
||||
response = post(BASE_URL, json={
|
||||
'query': """
|
||||
query ($id: Int) {
|
||||
Media (id: $id) {
|
||||
id
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
format
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
format
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
}
|
||||
}
|
||||
edges{
|
||||
relationType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
'variables': {"id": id}
|
||||
})
|
||||
if response.ok:
|
||||
return response.json()
|
||||
raise ValueError("Bad show")
|
||||
|
||||
def get_base_show(res):
|
||||
base = res["data"]["Media"]
|
||||
return SortableAnime(base["id"], base["startDate"]["year"], base["startDate"]["month"], base["startDate"]["day"], "BASE", base["title"], base["format"])
|
||||
|
||||
def process_shows(res):
|
||||
ls = []
|
||||
ls.append(get_base_show(res))
|
||||
for i,v in enumerate(res["data"]["Media"]["relations"]["nodes"]):
|
||||
ls.append(SortableAnime(v["id"], v["startDate"]["year"], v["startDate"]["month"], v["startDate"]["day"], res["data"]["Media"]["relations"]["edges"][i]["relationType"], v["title"], v["format"]))
|
||||
pass
|
||||
return ls
|
||||
|
||||
def main(query):
|
||||
items = []
|
||||
show_id = search(query)
|
||||
res = get_show(show_id)
|
||||
items.extend(process_shows(res))
|
||||
base_show = get_base_show(res)
|
||||
|
||||
if "PREQUEL" not in [i.type for i in items]:
|
||||
season = 1
|
||||
final_items = items
|
||||
else:
|
||||
ignore = []
|
||||
while True:
|
||||
f = False
|
||||
for i in items:
|
||||
if i.type == "PREQUEL" and i not in ignore:
|
||||
fi = [i for i in process_shows(get_show(i.id)) if i not in items]
|
||||
items.extend(fi)
|
||||
ignore.append(i)
|
||||
f = True
|
||||
if not f:
|
||||
break
|
||||
final_items = [i for i in items if i.frmt == "TV" and (i.type == "PREQUEL" or i.type == "SEQUEL" or i.type == "BASE")]
|
||||
if not base_show in final_items:
|
||||
final_items = [i for i in items if (i.type == "PREQUEL" or i.type == "SEQUEL" or i.type == "BASE")]
|
||||
final_items.sort(key=lambda i: i.timestamp)
|
||||
if base_show in final_items:
|
||||
season = final_items.index(base_show)
|
||||
if season:
|
||||
season += 1
|
||||
else:
|
||||
raise Exception("Cannot determine season")
|
||||
else:
|
||||
raise BaseShowNotInList(final_items)
|
||||
return season, base_show, final_items
|
||||
|
||||
if __name__ == "__main__":
|
||||
season, show, items = main(argv[1])
|
||||
base_title = items[0].title
|
||||
print("{}|{}|{}".format(base_title["romaji"], base_title["english"], season))
|
@ -4,8 +4,7 @@ function convert() {
|
||||
res=$(ffprobe -v error -show_entries stream=codec_type,codec_name -of compact "$1" | grep -s "subtitle");
|
||||
if [[ "$res" == *"ass"* ]]; then
|
||||
echo "Converting $1";
|
||||
ext="${1##*.}";
|
||||
ffmpeg -n -i "$1" -c:s srt "${i%.$ext}.srt" > /dev/null 2&> /dev/null;
|
||||
ffmpeg -n -i "$1" -c:s srt "${i%.$ext}.srt" > /dev/null
|
||||
else
|
||||
echo "$1 not ASS. Skipping";
|
||||
fi
|
||||
@ -15,10 +14,12 @@ if [[ -d "$1" ]]; then
|
||||
cd "$1";
|
||||
shopt -s globstar
|
||||
for i in **/*; do
|
||||
ext="${i##*.}";
|
||||
convert "$i";
|
||||
done
|
||||
elif [[ -f "$1" ]]; then
|
||||
cd $(dirname "$1");
|
||||
name=$(basename "$1");
|
||||
ext="${name##*.}";
|
||||
convert "$name";
|
||||
fi
|
||||
|
Loading…
x
Reference in New Issue
Block a user