Go to Vue3!

This commit is contained in:
CakesTwix 2024-09-23 19:15:53 +03:00
parent a205ebf3a4
commit 0ada059a22
Signed by: CakesTwix
GPG key ID: 7B11051D5CE19825
37 changed files with 981 additions and 220 deletions

27
.gitignore vendored
View file

@ -13,4 +13,29 @@ dist/
build/ build/
*.egg-info/ *.egg-info/
cookie.txt cookie.txt
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

44
app/__init__.py Normal file
View file

@ -0,0 +1,44 @@
import os
from flask import Flask, render_template, request, jsonify, redirect, url_for, current_app, send_file
from flask_caching import Cache
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,
)
from app.services.torrents import initiate_config, serialize_operation_result
#from .api import api_bp
from .client import client_bp
from .api import *
from .db import db
# App
app = Flask(__name__, static_folder='../frontend/dist/assets')
app.register_blueprint(client_bp)
app.register_blueprint(api_bp)
# Cache
cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
cache.init_app(app)
# Database
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///t2w.db"
db.init_app(app)
with app.app_context():
db.create_all()
# Production route
from .config import Config
app.logger.info('>>> {}'.format(Config.FLASK_ENV))
@app.route('/')
def index_client():
dist_dir = current_app.config['DIST_DIR']
entry = os.path.join(dist_dir, 'index.html')
return send_file(entry)

7
app/api/__init__.py Normal file
View file

@ -0,0 +1,7 @@
from flask import Blueprint
api_bp = Blueprint('api', __name__)
from .titles import *
from .toloka import *
from .config_api import *

13
app/api/config_api.py Normal file
View file

@ -0,0 +1,13 @@
from flask import jsonify
from app.services.torrents import initiate_config
from . import api_bp
@api_bp.route("/api/config/")
def config_route():
config = initiate_config()
config = dict(config.app_config)
for section in config.keys():
config[section] = dict(config[section])
return jsonify(config)

104
app/api/titles.py Normal file
View file

@ -0,0 +1,104 @@
import os
from flask import Blueprint, jsonify, request, current_app
from app.services.torrents import initiate_config, serialize_operation_result
from flask_sqlalchemy import SQLAlchemy
from app.db import db, ImagesCache
from app.models.request_data import RequestData
from . import api_bp
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,
)
@api_bp.route("/api/titles")
def titles_route():
sections = []
config = initiate_config()
titles = config.titles_config
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
titles[title]["codename"] = title
titles[title]["torrent_name"] = titles[title]["torrent_name"].replace(
'"', ""
)
sections.append(dict(titles[title]))
return jsonify(sections)
@api_bp.route("/api/add", methods=['POST'])
def add_route():
config = initiate_config()
requestData = RequestData(
url=request.json["tolokaUrl"],
season=request.json["seasonIndex"],
index=int(request.json["episodeIndex"].split('.')[0]),
correction=int(request.json["adjustedEpisodeNumber"]),
title=request.json["dirname"],
)
config.args = requestData
operation_result = add_release_by_url(config)
output = serialize_operation_result(operation_result)
return jsonify({"result": True})
@api_bp.route("/api/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} успішно видалений."
@api_bp.route("/api/update/", defaults={"codename": None})
@api_bp.route("/api/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}

8
app/api/toloka.py Normal file
View file

@ -0,0 +1,8 @@
from flask import jsonify
from app.services.torrents import initiate_config
from . import api_bp
@api_bp.route("/api/toloka/<id>")
def toloka_torrent_route(id):
config = initiate_config()
return jsonify(config.toloka.get_torrent('https://toloka.to/' + id))

View file

@ -1,152 +0,0 @@
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}

9
app/client.py Normal file
View file

@ -0,0 +1,9 @@
""" Client App """
import os
from flask import Blueprint, render_template
client_bp = Blueprint('client_app', __name__,
static_folder='../frontend/dist/assets/',
template_folder='../frontend/dist/',
)

24
app/config.py Normal file
View file

@ -0,0 +1,24 @@
"""
Global Flask Application Setting
See `.flaskenv` for default settings.
"""
import os
from app import app
class Config(object):
# If not set fall back to production for safety
FLASK_ENV = os.getenv('FLASK_ENV', 'production')
# Set FLASK_SECRET on your production Environment
SECRET_KEY = os.getenv('FLASK_SECRET', 'Secret')
APP_DIR = os.path.dirname(__file__)
ROOT_DIR = os.path.dirname(APP_DIR)
DIST_DIR = os.path.join(ROOT_DIR, 'frontend/dist')
if not os.path.exists(DIST_DIR):
raise Exception(
'DIST_DIR not found: {}'.format(DIST_DIR))
app.config.from_object('app.config.Config')

View file

@ -1,52 +0,0 @@
[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

14
app/db.py Normal file
View file

@ -0,0 +1,14 @@
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
db = SQLAlchemy()
class Base(DeclarativeBase, MappedAsDataclass):
pass
class ImagesCache(Base):
__tablename__ = "image_cache"
codename: Mapped[str] = mapped_column(primary_key=True)
image: Mapped[str] = mapped_column()

View file

@ -1,9 +0,0 @@
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

@ -8,10 +8,10 @@
logging = DEBUG logging = DEBUG
[transmission] [transmission]
username = username =
password = password =
port = 9091 port = 9091
host = host = 192.168.1.2
protocol = http protocol = http
rpc = /transmission/rpc rpc = /transmission/rpc
category = sonarr category = sonarr
@ -19,8 +19,8 @@ tag = tolokaAnime
[Toloka] [Toloka]
username = username =
password = password =
client = client = transmission
default_download_dir = default_download_dir = /media/HDD/Jellyfin/Anime
wait_time = 10 wait_time = 10
client_wait_time = 2 client_wait_time = 2

104
data/titles.ini Normal file
View file

@ -0,0 +1,104 @@
[HazurewakunoJoutaiIjouSkill]
episode_index = 1
season_number = 01
ext_name = .mkv
torrent_name = "Failure Frame: I Became the Strongest and Annihilated Everything with Low-Level Spells"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-09-16 14:40
release_group = GlassMoon
meta =
hash = 79419315387ad5121829276e41c148f86bc27b56
adjusted_episode_number = 0
guid = t679885
[Metalocalypse]
episode_index = 3
season_number = 02
ext_name = .mkv
torrent_name = "Metalocalypse"
download_dir = /media/HDD/Jellyfin/Cartoon
publish_date = 24-09-01 23:17
release_group = fanat22012
meta =
hash = b1d41c3d7671ca874a904896da3298b80fd4c648
adjusted_episode_number = 0
guid = t679197
[Noragami]
episode_index = 1
season_number = 01
ext_name = .mkv
torrent_name = "Noragami"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-08-04 13:26
release_group = Amanogawa
meta =
hash = a5b5c50cc287839af87785d49a90deaccd26672d
adjusted_episode_number = 0
guid = t680502
[MakeHeroinegaOosugiru]
episode_index = 1
season_number = 01
ext_name = .mkv
torrent_name = "Make Heroine ga Oosugiru"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-09-15 21:29
release_group = GlassMoon
meta =
hash = e40b7ed0662832ce1ac5201272ba4697c877d8f9
adjusted_episode_number = 0
guid = t680215
[GimaiSeikatsu]
episode_index = 1
season_number = 01
ext_name = .mkv
torrent_name = "Gimai Seikatsu"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-09-20 22:03
release_group = GlassMoon
meta =
hash = 833ce986aae5ead89ee65f90867bb3d87fc96ea2
adjusted_episode_number = 0
guid = t679864
[OokamitoKoushinryouMerchantMeetstheWiseWolf]
episode_index = 2
season_number = 01
ext_name = .mkv
torrent_name = "Ookami to Koushinryou: Merchant Meets the Wise Wolf"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-09-17 21:48
release_group = GlassMoon
meta =
hash = 2dc1117d464bc5a4363711b81a8027afbc456bca
adjusted_episode_number = 0
guid = t677900
[demon_academy]
episode_index = 2
season_number = 02
ext_name = .mkv
torrent_name = "The Misfit of Demon King Academy"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-09-01 17:06
release_group = FanVoxUA
meta =
hash = 4810c24d846237d638d7bfb33d6d3fba68d031d8
adjusted_episode_number = 0
guid = t664369
[AngelBeats]
episode_index = 0
season_number = 01
ext_name = .mkv
torrent_name = "Angel.Beats"
download_dir = /media/HDD/Jellyfin/Anime
publish_date = 24-09-19 23:53
release_group = stark62
meta =
hash = abb7518024aa0a875b8d0a7841121e84936e2120
adjusted_episode_number = 0
guid = t49611

24
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
frontend/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

1
frontend/README.md Normal file
View file

@ -0,0 +1 @@
# Toloka2Web v2 Vue Edition

13
frontend/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

26
frontend/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.5",
"beercss": "^3.6.13",
"material-dynamic-colors": "^1.1.2",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
"typescript": "^5.5.3",
"vite": "^5.4.1",
"vite-plugin-pages": "^0.32.3",
"vue-tsc": "^2.0.29"
}
}

1
frontend/public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

23
frontend/src/App.vue Normal file
View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import "beercss";
import "material-dynamic-colors";
import Navbar from './components/Navbar.vue'
function getCookie(id: string) {
let value = document.cookie.match('(^|;)?' + id + '=([^;]*)(;|$)');
return value ? unescape(value[2]) : null;
}
ui("mode", getCookie('my-mode')!);
ui("theme", getCookie('my-color')!);
</script>
<template>
<Navbar />
<main class="responsive">
<router-view />
</main>
</template>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View file

@ -0,0 +1,69 @@
<script setup lang="ts">
const pathname = location.pathname
</script>
<template>
<nav class="left drawer primary-text">
<header>
<nav>
<img src="https://www.beercss.com/favicon.png" class="circle">
<h6>Toloka2Web MD3</h6>
</nav>
</header>
<a :class="(pathname == '/') ? 'active' : ''" href="/">
<i>home</i>
<div>Головна</div>
</a>
<a :class="(pathname == '/add') ? 'active' : ''" href="/add">
<i>add</i>
<div>Додати</div>
</a>
<a :class="(pathname == '/settings') ? 'active' : ''" href="/settings">
<i>settings</i>
<div>Налаштування</div>
</a>
<div class="divider small-margin"></div>
<label>Інше</label>
<a :class="(pathname == '/about') ? 'active' : ''" href="/about">
<i>person</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>
</template>

16
frontend/src/main.ts Normal file
View file

@ -0,0 +1,16 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from '~pages'
const router = createRouter({
history: createWebHistory(),
routes
})
createApp(App)
.use(router)
.mount('#app')

View file

@ -0,0 +1,35 @@
<template>
<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>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
};
},
created() {
},
methods: {
}
}
</script>

140
frontend/src/pages/add.vue Normal file
View file

@ -0,0 +1,140 @@
<template>
<form @submit.prevent="searchTorrent">
<div class="field large prefix round fill">
<i class="front">search</i>
<input v-model="torrentId" type="text" placeholder="https://toloka.to/t680082">
</div>
</form>
<article v-if="Object.keys(torrentInfo).length > 0" class="no-padding">
<div class="grid large-space">
<div class="s12 m6 l3">
<img class="responsive" :src=torrentInfo.img>
</div>
<div class="s9">
<div class="padding">
<h5 class="primary-text">{{ torrentInfo.name }}</h5>
<hr class="medium">
<div>
<a class="chip no-margin"><i>language_gb_english</i><span>{{ torrentInfo.size }}</span></a>
<a class="chip no-margin"><i>signature</i><span>{{ torrentInfo.author }}</span></a>
<a class="chip no-margin"><i>calendar_month</i><span>{{ torrentInfo.date }}</span></a>
<a class="chip no-margin"><i>star_rate</i><span>{{ torrentInfo.rating }}</span></a>
<a class="chip no-margin"><i>thumb_up</i><span>{{ torrentInfo.thanks }}</span></a>
</div>
<hr class="large">
{{ torrentInfo.description }}
</div>
</div>
</div>
</article>
<article v-if="Object.keys(torrentInfo).length > 0">
<form @submit.prevent="addTitle">
<div class="grid">
<div class="field label prefix border s6">
<i>folder</i>
<input type="text" v-model="dirname">
<label>Назва директорії</label>
</div>
<div class="field label prefix border s6">
<i>numbers</i>
<select v-model="seasonIndex">
<option v-for="n in 10">{{ n }}</option>
</select>
<label>Індекс сезону</label>
</div>
<div class="field label prefix border s6">
<i>numbers</i>
<select v-model="episodeIndex">
<option v-for="n in extractNumbers(torrentInfo.files[0].file_name)">{{ n }}</option>
</select>
<label>Індекс епізоду</label>
</div>
<div class="field label prefix border s6">
<i>route</i>
<input type="text" v-model="filepath">
<label>Місце завантаження</label>
</div>
<div class="field label prefix border s6">
<i>numbers</i>
<input type="number" v-model="adjustedEpisodeNumber" value="0">
<label>Скільки додати до епізоду</label>
</div>
<button class="border small-round s12" type="submit">
<i>add</i>
<span>Додати</span>
</button>
</div>
</form>
</article>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
torrentId: '',
torrentInfo: {},
config_: {},
dirname: '',
seasonIndex: 0,
episodeIndex: 1,
filepath: '',
adjustedEpisodeNumber: 0,
};
},
created() {
this.getConfig()
},
methods: {
async searchTorrent() {
void await axios.get(`/api/toloka/${this.torrentId.replace(`https://toloka.to`, ``)}`).then(response => {
this.torrentInfo = response.data
this.dirname = this.torrentInfo.files[0].folder_name
})
},
async getConfig() {
void await axios.get(`/api/config/`).then(response => {
this.config_ = response.data
this.filepath = this.config_.Toloka.default_download_dir
})
},
extractNumbers(input) {
const numbers = input.match(/\d+/g);
return numbers ? numbers.map((num, index) => `${index + 1}. ${num}`) : [];
},
async addTitle() {
const formData = {
dirname: this.dirname,
seasonIndex: this.seasonIndex,
episodeIndex: this.episodeIndex,
filepath: this.filepath,
adjustedEpisodeNumber: this.adjustedEpisodeNumber,
tolokaUrl: this.torrentId
};
try {
const response = await axios.post('/api/add', formData);
} catch (error) {
console.error("Error~", error);
}
}
}
}
</script>

View file

@ -0,0 +1,149 @@
<template>
<form @submit.prevent="filterTitles">
<div class="field large prefix round fill">
<i class="front">search</i>
<input v-model="search" type="text">
</div>
</form>
<nav>
<a class="chip" onclick="updateByCodename('')">
<i id="update_all_icon">done_all</i>
<span>Завантажити нові серії</span>
</a>
<a class="chip" @click="fetchTorrents">
<i>restart_alt</i>
<span>Оновити список</span>
</a>
<div class="max"></div>
<button class="round fill">
<span>Сортування</span>
<i>arrow_drop_down</i>
<menu id="sort-menu">
<a @click="sortTitles('name')">За назвою</a>
<a @click="sortTitles('date')">За датою</a>
<a @click="sortTitles('release')">За релізером</a>
</menu>
</button>
</nav>
<article class="no-padding" v-for="title in titles">
<div class="grid">
<div class="s12 m6 l3">
<img class="responsive " :src=title.image>
</div>
<div class="s9">
<div class="padding">
<h5 class="primary-text">{{ title.torrent_name }} ({{ title.codename }})</h5>
<hr class="medium">
<div>
<a class="chip no-margin"><i>route</i><span>{{ title.download_dir }}</span></a>
<a class="chip no-margin"><i>signature</i><span>{{ title.release_group }}</span></a>
<a class="chip no-margin"><i>calendar_month</i><span>{{ title.publish_date }}</span></a>
<a class="chip no-margin"><i>tag</i><span>{{ title.hash }}</span></a>
</div>
<hr class="large">
<div>
<a class="chip primary-border no-margin" :href="'https://toloka.to/' + title.guid">
<i>link</i>
<span>Посилання</span>
</a>
<a class="chip green-border no-margin" @click="updateByCodename(title.codename)">
<i>update</i>
<span>Оновити</span>
</a>
<a class="chip amber-border no-margin" @click="editByCodename(title.codename)">
<i>edit</i>
<span>Редагувати</span>
</a>
<a class="chip red-border no-margin" @click="deleteByCodename(title.codename)">
<i>delete</i>
<span>Видалити</span>
</a>
</div>
</div>
</div>
</div>
</article>
<dialog :class="{ 'active': isUpdated }">
<h5 class="primary-text">{{ update.response_code }}</h5>
<div v-for="operation in update.operation_logs">
<p>* {{ operation }}</p>
</div>
<nav class="right-align no-space">
<button @click="updateOk" class="transparent link">Гаразд</button>
</nav>
</dialog>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
titles: [], // Titles list
search: '', // Search bar
update: {}, // Update stuff, notif
isUpdated: false // Update notif
};
},
created() {
this.fetchTorrents()
},
methods: {
async fetchTorrents() {
void await axios.get(`/api/titles`).then(response => {
this.titles = response.data
})
},
filterTitles() {
if (!this.search || typeof this.search !== 'string') {
this.fetchTorrents()
}
const filteredTitles = this.titles.filter(title =>
title.torrent_name && title.torrent_name.includes(this.search)
);
this.titles = filteredTitles;
},
async updateByCodename(codename) {
void await axios.get(`/api/update/${codename}`).then(response => {
this.update = response.data
this.isUpdated = true
})
},
async deleteByCodename(codename) {
void await axios.get(`/api/delete/${codename}`).then(response => {
this.update.response_code = `Видалення`
this.update.operation_logs = []
this.update.operation_logs[0] = response.data
this.fetchTorrents()
this.isUpdated = true
})
},
updateOk(codename) {
this.isUpdated = false
},
sortTitles(criteria) {
if (criteria === 'name') {
this.titles.sort((a, b) => a.torrent_name.localeCompare(b.torrent_name));
} else if (criteria === 'date') {
this.titles.sort((a, b) => a.publish_date.localeCompare(b.publish_date));
} else if (criteria === 'release') {
this.titles.sort((a, b) => a.release_group.localeCompare(b.release_group));
}
ui('#sort-menu');
}
}
}
</script>

View file

@ -0,0 +1,43 @@
<template>
<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 Vue Version</a>
<hr>
<a class="row wave"><i>conversion_path</i> v1.0.0</a>
</article>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
};
},
created() {
},
methods: {
}
}
</script>

0
frontend/src/style.css Normal file
View file

2
frontend/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pages/client" />

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View file

@ -0,0 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/App.vue","./src/components/Navbar.vue","./src/pages/index.vue"],"version":"5.6.2"}

7
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View file

@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.6.2"}

18
frontend/vite.config.ts Normal file
View file

@ -0,0 +1,18 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Pages from 'vite-plugin-pages'
// https://vitejs.dev/config/
export default ({ }) => {
return defineConfig({
plugins: [vue(), Pages()],
server: {
proxy: {
'/api': {
// Forward frontend dev server request for /api to flask dev server
target: 'http://localhost:5000/',
}
}
},
})
}

3
run.py Normal file
View file

@ -0,0 +1,3 @@
from app import app
app.run(port=5000)