added anime namer

This commit is contained in:
marios8543 2020-05-07 16:56:40 +03:00
parent e5acec3513
commit 7fbac97e77
8 changed files with 508 additions and 2 deletions

8
anime-namer/Dockerfile Normal file
View 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
View 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"))

View 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

View 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>

View 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>

View 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
View 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))

View File

@ -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