Go to Vue3!
This commit is contained in:
parent
a205ebf3a4
commit
0ada059a22
37 changed files with 981 additions and 220 deletions
25
.gitignore
vendored
25
.gitignore
vendored
|
@ -14,3 +14,28 @@ 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
44
app/__init__.py
Normal 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
7
app/api/__init__.py
Normal 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
13
app/api/config_api.py
Normal 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
104
app/api/titles.py
Normal 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
8
app/api/toloka.py
Normal 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))
|
152
app/app.py
152
app/app.py
|
@ -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
9
app/client.py
Normal 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
24
app/config.py
Normal 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')
|
|
@ -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
14
app/db.py
Normal 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()
|
|
@ -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()
|
|
|
@ -11,7 +11,7 @@ logging = DEBUG
|
||||||
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
|
||||||
|
@ -20,7 +20,7 @@ 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
104
data/titles.ini
Normal 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
24
frontend/.gitignore
vendored
Normal 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
3
frontend/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
1
frontend/README.md
Normal file
1
frontend/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Toloka2Web v2 Vue Edition
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal 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
26
frontend/package.json
Normal 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
1
frontend/public/vite.svg
Normal 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
23
frontend/src/App.vue
Normal 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>
|
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal 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 |
69
frontend/src/components/Navbar.vue
Normal file
69
frontend/src/components/Navbar.vue
Normal 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
16
frontend/src/main.ts
Normal 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')
|
||||||
|
|
35
frontend/src/pages/about.vue
Normal file
35
frontend/src/pages/about.vue
Normal 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
140
frontend/src/pages/add.vue
Normal 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>
|
149
frontend/src/pages/index.vue
Normal file
149
frontend/src/pages/index.vue
Normal 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>
|
43
frontend/src/pages/settings.vue
Normal file
43
frontend/src/pages/settings.vue
Normal 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
0
frontend/src/style.css
Normal file
2
frontend/src/vite-env.d.ts
vendored
Normal file
2
frontend/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pages/client" />
|
24
frontend/tsconfig.app.json
Normal file
24
frontend/tsconfig.app.json
Normal 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"]
|
||||||
|
}
|
1
frontend/tsconfig.app.tsbuildinfo
Normal file
1
frontend/tsconfig.app.tsbuildinfo
Normal 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
7
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
22
frontend/tsconfig.node.json
Normal file
22
frontend/tsconfig.node.json
Normal 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"]
|
||||||
|
}
|
1
frontend/tsconfig.node.tsbuildinfo
Normal file
1
frontend/tsconfig.node.tsbuildinfo
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"root":["./vite.config.ts"],"version":"5.6.2"}
|
18
frontend/vite.config.ts
Normal file
18
frontend/vite.config.ts
Normal 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
3
run.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
app.run(port=5000)
|
Loading…
Reference in a new issue