Initial commit

This commit is contained in:
CakesTwix 2024-08-23 17:05:31 +03:00
commit bd0a7a32b2
Signed by: CakesTwix
GPG key ID: 7B11051D5CE19825
12 changed files with 903 additions and 0 deletions

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
.venv/
*.pyc
__pycache__/
instance/
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
cookie.txt

122
app/app.py Normal file
View file

@ -0,0 +1,122 @@
from flask import Flask, render_template, request, jsonify, redirect, url_for
import time
from services.torrents import initiate_config, serialize_operation_result
from models.request_data import RequestData
from flask_caching import Cache
from flask_sqlalchemy import SQLAlchemy
from toloka2MediaServer.main_logic import (
add_release_by_url, update_release_by_name, update_releases,
search_torrents, get_torrent as get_torrent_external,
add_torrent as add_torrent_external
)
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///t2w.db"
cache.init_app(app)
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
class Base(DeclarativeBase, MappedAsDataclass):
pass
db = SQLAlchemy(app, model_class=Base)
from models.db import ImagesCache
with app.app_context():
db.create_all()
@app.route("/")
@cache.cached(timeout=1)
def root_route():
sections = []
config = initiate_config()
titles = config.titles_config
new_torrent = 0
for title in titles.sections():
if request.args.get('query', '') in title:
image_cache = db.session.execute(db.select(ImagesCache).filter_by(codename=title)).scalar_one_or_none()
if image_cache:
titles[title]["image"] = image_cache.image
else:
toloka_torrent = config.toloka.get_torrent(f"https://toloka.to/{titles[title]["guid"]}")
toloka_img = f"https:{toloka_torrent.img}" if toloka_torrent.img.startswith("//") else toloka_torrent.img
db.session.add(ImagesCache(title, toloka_img))
db.session.commit()
titles[title]["image"] = toloka_img
# Config data
titles[title]["codename"] = title
titles[title]["torrent_name"] = titles[title]["torrent_name"].replace("\"", "")
sections.append(titles[title])
return render_template('index.html', titles = sections, new_torrent = new_torrent)
# First stage
@app.route("/add")
def add_route():
config = initiate_config()
if request.args.get('query'):
torrent = config.toloka.get_torrent(request.args.get('query'))
return render_template('add.html', torrent = torrent, episode_integers = [i for i in ''.join((ch if ch.isdigit() else ' ') for ch in f"{torrent.files[0].folder_name}/{torrent.files[0].file_name}").split()], default_dir = config.app_config.get("Toloka", "default_download_dir"))
if len(request.args) == 6:
requestData = RequestData(
url = request.args["toloka_url"],
season = request.args["season-index"],
index = int(request.args["episode-index"]),
correction = int(request.args['adjusted-episode-number']),
title = request.args["dirname"],
)
config.args = requestData
operation_result = add_release_by_url(config)
output = serialize_operation_result(operation_result)
return redirect(url_for('root_route'))
return render_template('add.html')
@app.route("/about")
def about_route():
return render_template('about.html')
@app.route("/settings")
def settings_route():
return render_template('settings.html')
@app.route("/delete/<codename>")
def delete_route(codename):
config = initiate_config()
titles = config.titles_config
titles.remove_section(codename)
with open("data/titles.ini", "w") as f:
titles.write(f)
return f"{codename} успішно видалений."
@app.route("/update/", defaults={'codename': None})
@app.route("/update/<codename>")
def update_route(codename):
# Process the name to update release
try:
config = initiate_config()
requestData = RequestData(
codename = codename
)
if codename:
config.args = requestData
operation_result = update_release_by_name(config)
else:
config.args = RequestData()
operation_result = update_releases(config)
output = serialize_operation_result(operation_result)
return output
except Exception as e:
message = f'Error: {str(e)}'
return {"error": message}

26
app/data/app.ini Normal file
View file

@ -0,0 +1,26 @@
[Python]
# NOTSET
# DEBUG
# INFO
# WARNING
# ERROR
# CRITICAL
logging = DEBUG
[transmission]
username =
password =
port = 9091
host =
protocol = http
rpc = /transmission/rpc
category = sonarr
tag = tolokaAnime
[Toloka]
username =
password =
client =
default_download_dir =
wait_time = 10
client_wait_time = 2

52
app/data/titles.ini Normal file
View file

@ -0,0 +1,52 @@
[TenseishitaraSlimeDattaKen]
episode_index = 2
season_number = 03
ext_name = .mkv
torrent_name = "Tensei shitara Slime Datta Ken"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-08-16 21:57
release_group = FanVoxUA
meta =
hash = 18a83050fa77ae155d0df927c6142824c35094bb
adjusted_episode_number = 0
guid = t678039
[DEADDEADDEMONSDEDEDEDEDESTRUCTION]
episode_index = 0
season_number = 01
ext_name = .mkv
torrent_name = "DEAD DEAD DEMONS DEDEDEDE DESTRUCTION"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-08-18 12:01
release_group = FanVoxUA
meta =
hash = 40c5fbeed2f1c4c11fb7247eb81e72b70255f871
adjusted_episode_number = 0
guid = t679732
[ScottPilgrimTakesOff]
episode_index = 4
season_number = 01
ext_name = .mkv
torrent_name = "Scott Pilgrim Takes Off"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-06-29 19:41
release_group = Altron320
meta =
hash = 25946318d080809e65f32249bd3184d265af2146
adjusted_episode_number = 0
guid = t679822
[VinlandSaga]
episode_index = 2
season_number = 01
ext_name = .mkv
torrent_name = "Vinland Saga"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 20-06-29 20:08
release_group = 11FrYkT
meta =
hash = 2d1c961f8899156ff4ef995b1ad9b03bc75442d6
adjusted_episode_number = 0
guid = t111251

8
app/models/db.py Normal file
View file

@ -0,0 +1,8 @@
from app import db
from sqlalchemy.orm import Mapped, mapped_column
class ImagesCache(db.Model):
__tablename__ = "image_cache"
codename: Mapped[str] = mapped_column(primary_key=True)
image: Mapped[str] = mapped_column()

View file

@ -0,0 +1,16 @@
class RequestData:
url: str = ""
season: int = 0
index: int = 0
correction: int = 0
title: str = ""
codename: str = ""
force: bool = False
def __init__(self, url = "", season = 0, index = 0, correction = 0, title = "", codename = "", force=False):
self.url = url
self.season = season
self.index = index
self.correction = correction
self.title = title
self.codename = codename
self.force = force

38
app/services/torrents.py Normal file
View file

@ -0,0 +1,38 @@
from toloka2MediaServer.config_parser import load_configurations, get_toloka_client
from toloka2MediaServer.logger_setup import setup_logging
from toloka2MediaServer.models.config import Config
from toloka2MediaServer.clients.dynamic import dynamic_client_init
def initiate_config():
app_config_path='data/app.ini'
title_config_path='data/titles.ini'
logger_path = 'data/app_web.log'
app_config, titles_config, application_config = load_configurations(app_config_path,title_config_path)
toloka=get_toloka_client(application_config)
logger=setup_logging(logger_path)
config = Config(
logger=logger,
toloka=toloka,
app_config=app_config,
titles_config=titles_config,
application_config=application_config
)
client = dynamic_client_init(config)
config.client = client
return config
def serialize_operation_result(operation_result):
return {
"operation_type": operation_result.operation_type.name if operation_result.operation_type else None,
"torrent_references": [str(torrent) for torrent in operation_result.torrent_references],
"titles_references": [str(titles) for titles in operation_result.titles_references],
"status_message": operation_result.status_message,
"response_code": operation_result.response_code.name if operation_result.response_code else None,
"operation_logs": operation_result.operation_logs,
"start_time": operation_result.start_time.isoformat() if operation_result.start_time else None,
"end_time": operation_result.end_time.isoformat() if operation_result.end_time else None
}

90
app/templates/about.html Normal file
View file

@ -0,0 +1,90 @@
{% extends "base.html" %} {% block nav %}
<nav class="left drawer">
<header>
<nav>
<img src="https://www.beercss.com/favicon.png" class="circle">
<h6>Toloka2Web MD3</h6>
</nav>
</header>
<a href="/">
<i>home</i>
<div>Головна</div>
</a>
<a href="add">
<i>add</i>
<div>Додати</div>
</a>
<a href="settings">
<i>settings</i>
<div>Налаштування</div>
</a>
<div class="divider small-margin"></div>
<label>Інше</label>
<a class="active" href="about">
<i>person</i>
<div>Про застосунок</div>
</a>
<a href="help">
<i>help</i>
<div>Допомога</div>
</a>
</nav>
<nav class="left m">
<header>
<img src="https://www.beercss.com/favicon.png" class="circle">
</header>
<a>
<i>home</i>
<div>Home</div>
</a>
<a>
<i>search</i>
<div>Search</div>
</a>
<a>
<i>settings</i>
<div>Settings</div>
</a>
<a>
<i>more_vert</i>
<div>More</div>
</a>
</nav>
<nav class="bottom s">
<a>
<i>home</i>
</a>
<a>
<i>search</i>
</a>
<a>
<i>settings</i>
</a>
<a>
<i>more_vert</i>
</a>
</nav>
{% endblock %} {% block content %}
<div class="padding absolute center middle">
<article class="medium middle-align center-align">
<div>
<i class="extra">cloud</i>
<h5 class="primary-text">Toloka2Web v2</h5>
<p class="secondary-text">Зручний сайт для завантаження аніме до медіасерверу Jellyfin</p>
<div class="space"></div>
<nav class="center-align">
<a href="https://github.com"><i class="chip circle">code</i></a>
<a href="https://github.com"><i class="chip circle">bug_report</i></a>
<a href="https://mastodon.com"><i class="chip circle">diversity_3</i></a>
</nav>
</div>
</article>
</div>
{% endblock %}

202
app/templates/add.html Normal file
View file

@ -0,0 +1,202 @@
{% extends "base.html" %} {% block nav %}
<nav class="left drawer min">
<header>
<nav>
<img src="https://www.beercss.com/favicon.png" class="circle">
<h6>Toloka2Web MD3</h6>
</nav>
</header>
<a href="/">
<i>home</i>
<div>Головна</div>
</a>
<a class="active" href="add">
<i>add</i>
<div>Додати</div>
</a>
<a href="settings">
<i>settings</i>
<div>Налаштування</div>
</a>
<div class="divider small-margin"></div>
<label>Інше</label>
<a href="about">
<i>person</i>
<div>Про застосунок</div>
</a>
<a href="help">
<i>help</i>
<div>Допомога</div>
</a>
</nav>
<nav class="left m">
<header>
<img src="https://www.beercss.com/favicon.png" class="circle">
</header>
<a>
<i>home</i>
<div>Home</div>
</a>
<a>
<i>search</i>
<div>Search</div>
</a>
<a>
<i>settings</i>
<div>Settings</div>
</a>
<a>
<i>more_vert</i>
<div>More</div>
</a>
</nav>
<nav class="bottom s">
<a>
<i>home</i>
</a>
<a>
<i>search</i>
</a>
<a>
<i>settings</i>
</a>
<a>
<i>more_vert</i>
</a>
</nav>
{% endblock %} {% block content %}
<p class="large-text">
<h6><i>add</i> Додати новий контент</h6>
</p>
<nav class="scroll">
<div class="center-align">
<button class="circle small" id="step_1"><i>search</i></button>
<div class="small-margin">Знайти торрент</div>
</div>
<hr class="max">
<div class="center-align">
<button class="circle small" id="step_2"><i>info</i></button>
<div class="small-margin">Метадані</div>
</div>
<hr class="max">
<div class="center-align">
<button class="circle small" disabled id="step_3"><i>add</i></button>
<div class="small-margin">Додати WIP</div>
</div>
</nav>
<form action="/add" id="search_form">
<div class="field large prefix round fill">
<i class="front">search</i>
<input id="search-box" name="query" type="text" placeholder="https://toloka.to/t680082">
</menu>
</div>
</form>
{% if torrent %}
<article id="torrent_card" class="no-padding">
<div class="grid large-space">
<div class="s12 m6 l3">
<img class="responsive" src={{torrent.img}}>
</div>
<div class="s9">
<div class="padding">
<h5 class="primary-text">{{torrent.name}}</h5>
<hr class="medium">
<div>
<a class="chip no-margin"><i>language_gb_english</i><span>{{torrent.size}}</span></a>
<a class="chip no-margin"><i>signature</i><span>{{torrent.author}}</span></a>
<a class="chip no-margin"><i>calendar_month</i><span>{{torrent.date}}</span></a>
<a class="chip no-margin"><i>star_rate</i><span>{{torrent.rating}}</span></a>
<a class="chip no-margin"><i>thumb_up</i><span>{{torrent.thanks}}</span></a>
</div>
<hr class="large">
{{torrent.description}}
<hr class="large">
<div>
<a class="green no-margin button" onclick="twoStage('{{torrent.url}}')">
<i>add</i>
<span>Додати</span>
</a>
</div>
</div>
</div>
</div>
</article>
<article class="center-align" style="display: none;" id="two_stage">
<form action="/add">
<div class="grid">
<div class="field label prefix border s6">
<i>folder</i>
<input type="text" name="dirname" value="{{torrent.files[0].folder_name}}">
<label>Назва директорії</label>
</div>
<div class="field label prefix border s6">
<i>numbers</i>
<select name="season-index">
{% for season in range(1, 10) %}
<option>{{season}}</option>
{% endfor %}
</select>
<label>Індекс сезону</label>
</div>
<div class="field label prefix border s6">
<i>numbers</i>
<select name="episode-index">
{% for option_index in range(episode_integers|length) %}
<option value="{{option_index + 1}}">{{episode_integers[option_index]}}</option>
{% endfor %}
</select>
<label>Індекс епізоду</label>
</div>
<div class="field label prefix border s6">
<i>route</i>
<input type="text" name="filepath" value="{{default_dir}}">
<label>Місце завантаження</label>
</div>
<input style="display: none;" type="text" name="toloka_url" value="{{torrent.url}}">
<div class="field label prefix border s6">
<i>numbers</i>
<input type="number" name="adjusted-episode-number" value="0">
<label>Скільки додати до епізоду</label>
</div>
<button class="border small-round s12" type="submit">
<i>add</i>
<span>Додати</span>
</button>
</div>
</form>
</article>
{% endif %}
<script>
function twoStage() {
document.getElementById("search_form").remove();
document.getElementById("torrent_card").remove();
document.getElementById("step_1").innerHTML = "<i>done</i>"
$("#two_stage").toggle();
}
</script>
{% endblock %}

44
app/templates/base.html Normal file
View file

@ -0,0 +1,44 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="google" content="notranslate">
<title>Toloka2Web</title>
<link href="https://cdn.jsdelivr.net/npm/beercss@3.6.13/dist/cdn/beer.min.css" rel="stylesheet">
<script type="module" src="https://cdn.jsdelivr.net/npm/beercss@3.6.13/dist/cdn/beer.min.js"></script>
<script type="module"
src="https://cdn.jsdelivr.net/npm/material-dynamic-colors@1.1.2/dist/cdn/material-dynamic-colors.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js"></script>
</head>
<body class="dark">
{% block nav %}{% endblock %}
<header class="surface-dim">
<nav>
<h5 class="max"></h5>
<a class="chip circle"><i>palette</i><input id="color-picker" type="color"></a>
<a class="chip circle" onclick="document.body.classList.toggle('dark');"><i>light_mode</i></a>
</nav>
</header>
<main class="responsive">
{% block content %}{% endblock %}
</main>
<script>
addEventListener("DOMContentLoaded", function startup() {
colorPicker = document.querySelector("#color-picker");
colorPicker.addEventListener("change", updateAll, false);
ui("theme", Cookies.get('my-color'));
})
function updateAll(event) {
ui("theme", event.target.value);
Cookies.set('my-color', event.target.value);
}
</script>
</body>
</html>

191
app/templates/index.html Normal file
View file

@ -0,0 +1,191 @@
{% extends "base.html" %}
{% block nav %}
<nav class="left drawer">
<header>
<nav>
<img src="https://www.beercss.com/favicon.png" class="circle">
<h6>Toloka2Web MD3</h6>
</nav>
</header>
<a class="active" href="/">
<i>home</i>
<div>Головна</div>
</a>
<a href="add">
<i>add</i>
<div>Додати</div>
</a>
<a href="settings">
<i>settings</i>
<div>Налаштування</div>
</a>
<div class="divider small-margin"></div>
<label>Інше</label>
<a href="about">
<i>person</i>
<div>Про застосунок</div>
</a>
<a href="help">
<i>help</i>
<div>Допомога</div>
</a>
</nav>
<nav class="left m">
<header>
<img src="https://www.beercss.com/favicon.png" class="circle">
</header>
<a>
<i>home</i>
<div>Home</div>
</a>
<a>
<i>search</i>
<div>Search</div>
</a>
<a>
<i>settings</i>
<div>Settings</div>
</a>
<a>
<i>more_vert</i>
<div>More</div>
</a>
</nav>
<nav class="bottom s">
<a>
<i>home</i>
</a>
<a>
<i>search</i>
</a>
<a>
<i>settings</i>
</a>
<a>
<i>more_vert</i>
</a>
</nav>
{% endblock %}
{% block content %}
<p class="large-text">
<h6><i>hub</i> Торенти</h6>
</p>
<button class="round fill">
<span>Сортування</span>
<i>arrow_drop_down</i>
<menu>
<a href="search?query=&sort=name">За назвою</a>
<a href="search?query=&sort=date">За датою</a>
<a href="search?query=&sort=releasename">За релізером</a>
</menu>
</button>
<p class="large-text">
<h6><i>work</i> Дії</h6>
</p>
<a class="chip" onclick="updateByCodename('')">
<i id="update_all_icon">done_all</i>
<span>Оновити</span>
</a>
<form action="/">
<div class="field large prefix round fill">
<i class="front">search</i>
<input id="search-box" name="query" type="text">
</menu>
</div>
</form>
{%for item in titles%}
<article class="no-padding" id="{{item.codename}}">
<div class="grid">
<div class="s12 m6 l3">
<img class="responsive " src={{item.image}}>
</div>
<div class="s9">
<div class="padding">
<h5 class="primary-text">{{item.torrent_name}} ({{item.codename}})</h5>
<hr class="medium">
<div>
<a class="chip no-margin"><i>route</i><span>{{item.download_dir}}</span></a>
<a class="chip no-margin"><i>signature</i><span>{{item.release_group}}</span></a>
<a class="chip no-margin"><i>calendar_month</i><span>{{item.publish_date}}</span></a>
<a class="chip no-margin"><i>tag</i><span>{{item.hash}}</span></a>
</div>
<hr class="large">
<div>
<a class="chip primary-border no-margin" href="https://toloka.to/{{item.guid}}">
<i>link</i>
<span>Посилання</span>
</a>
<a class="chip green-border no-margin" onclick="updateByCodename('{{item.codename}}')">
<i>update</i>
<span>Оновити</span>
</a>
<a class="chip amber-border no-margin" onclick="editByCodename('{{item.codename}}')">
<i>edit</i>
<span>Редагувати</span>
</a>
<a class="chip red-border no-margin" onclick="deleteByCodename('{{item.codename}}')">
<i>delete</i>
<span>Видалити</span>
</a>
</div>
</div>
</div>
</div>
</article>
{%endfor%}
<dialog id="dialog">
<h5 class="primary-text" id="torrent_title">Інформація</h5>
<div id="torrent_info"></div>
<nav class="right-align no-space">
<button class="transparent link" onclick="document.querySelector('#dialog').close();">Ок</button>
</nav>
</dialog>
<script>
function deleteByCodename(codename) {
fetch(`/delete/${codename}`);
document.getElementById(codename).remove();
}
function editByCodename(codename) {
location.href = (`/edit/${codename}`);
}
function updateByCodename(codename) {
if (codename == "") {
$('#update_all_icon').html("update")
}
$.ajax({
url: `/update/${codename}`,
type: 'GET',
success: function (response) {
if (response.hasOwnProperty("error")) {
$('#torrent_title').html("Помилка")
$('#torrent_info').html(`${response.error}`)
document.querySelector('#dialog').showModal();
} else {
$('#update_all_icon').html("done_all")
var operation_html = ""
document.querySelector('#dialog').showModal();
for (var i = 0; i < response.operation_logs.length; i++) {
operation_html += `* ${response.operation_logs[i]}<br>`
}
$('#torrent_info').html(`${operation_html}`)
}
},
error: function (result) {
document.querySelector('#dialog').showModal();
}
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,98 @@
{% extends "base.html" %} {% block nav %}
<nav class="left drawer">
<header>
<nav>
<img src="https://www.beercss.com/favicon.png" class="circle">
<h6>Toloka2Web MD3</h6>
</nav>
</header>
<a href="/">
<i>home</i>
<div>Головна</div>
</a>
<a href="add">
<i>add</i>
<div>Додати</div>
</a>
<a class="active" href="settings">
<i>settings</i>
<div>Налаштування</div>
</a>
<div class="divider small-margin"></div>
<label>Інше</label>
<a href="about">
<i>person</i>
<div>Про застосунок</div>
</a>
<a href="help">
<i>help</i>
<div>Допомога</div>
</a>
</nav>
<nav class="left m">
<header>
<img src="https://www.beercss.com/favicon.png" class="circle">
</header>
<a>
<i>home</i>
<div>Home</div>
</a>
<a>
<i>search</i>
<div>Search</div>
</a>
<a>
<i>settings</i>
<div>Settings</div>
</a>
<a>
<i>more_vert</i>
<div>More</div>
</a>
</nav>
<nav class="bottom s">
<a>
<i>home</i>
</a>
<a>
<i>search</i>
</a>
<a>
<i>settings</i>
</a>
<a>
<i>more_vert</i>
</a>
</nav>
{% endblock %} {% block content %}
<p class="large-text">
<h6><i>settings</i> Налаштування WIP</h6>
</p>
<div class="large-space"></div>
<article class="primary-text">
<a class="row wave"><i>person</i> Обліковий запис Tokoka.to</a>
<hr>
<a class="row wave"><i>hub</i> BitTorrent</a>
<hr>
<a class="row wave"><i>comic_bubble</i> Аніме</a>
<hr>
<a class="row wave"><i>bug_report</i> Дебагінг</a>
</article>
<article class="secondary-text">
<a class="row wave"><i>title</i> Toloka2Web v2</a>
<hr>
<a class="row wave"><i>conversion_path</i> v0.0.1</a>
</article>
{% endblock %}