async def login() -> Any: if await current_user.is_authenticated: return redirect(url_for("ux.index")) if request.method == "GET": fluent = get_injector(["login"]) return await render_template("login.html", **{"_": fluent.format_value}) else: resources = [ "login" ] fluent = get_injector(resources) form = await request.form username = form.get("username") password = form.get("password") if username is None or password is None: await flash(fluent._("form-missing-data")) return redirect(url_for("ux.login")) async with app.acquire_db() as con: await con.execute(""" SELECT id, password_hash FROM users WHERE LOWER(username) = ?; """, username.lower()) user_data = await con.fetchone() if not user_data: await flash(fluent._("invalid-credentials")) return redirect(url_for("ux.login")) try: hasher.verify(user_data["password_hash"], password) except VerifyMismatchError: await flash(fluent._("invalid-credentials")) return redirect(url_for("ux.login")) if hasher.check_needs_rehash(user_data["password_hash"]): async with app.acquire_db() as con: await con.execute(""" UPDATE users SET password_hash=? WHERE username=?; """, hasher.hash(password), username) remember = form.get("remember", False) login_user(User(user_data["id"]), remember=remember) return redirect(url_for("ux.index"))
async def config_encode() -> APIResponse: async with app.acquire_db() as con: await con.execute(""" INSERT OR IGNORE INTO encode_config ( id ) VALUES (0); """) if request.method == "PATCH": arguments = await request.get_json() CFG_KEYS = ("enabled", "quality_preset", "speed_preset", "maximum_encodes", "retry_on_fail") if any(k not in CFG_KEYS for k in arguments.keys()): return APIResponse(status=400, error="Invalid configuration keys.") elif arguments.get("speed_preset" ) and arguments["speed_preset"] not in VALID_SPEEDS: return APIResponse(status=400, error="Invalid speed preset.") elif arguments.get( "quality_preset") and arguments["quality_preset"] not in ( "high", "low", "moderate"): return APIResponse(status=400, error="Invalid quality preset.") elif arguments.get("maximum_encodes") and int( arguments["maximum_encodes"]) <= 0: arguments["maximum_encodes"] = 1 async with app.acquire_db() as con: sets = ", ".join(f"{col} = ?" for col in arguments.keys()) await con.execute( f""" UPDATE encode_config SET {sets} WHERE id = 0; """, *arguments.values()) async with app.acquire_db() as con: await con.execute(""" SELECT enabled, quality_preset, speed_preset, maximum_encodes, retry_on_fail FROM encode_config; """) cfg = await con.fetchone() cfg = dict(cfg) cfg["has_ffmpeg"] = await app.encoder.has_ffmpeg() return APIResponse(status=200, result=cfg)
async def get(self, show_id: int, entry_id: Optional[int]) -> APIResponse: """ Retrieve all entries or a single entry for a specified show. Returns ------- APIResponse A dict or a list of dict containing the requested entry information. """ if entry_id is None: async with app.acquire_db() as con: await con.execute(""" SELECT id, episode, current_state, torrent_hash FROM show_entry WHERE show_id=?; """, show_id) entries = await con.fetchall() return APIResponse( result=[dict(record) for record in entries] ) else: async with app.acquire_db() as con: await con.execute(""" SELECT id, episode, current_state, torrent_hash FROM show_entry WHERE id=?; """, entry_id) entry = await con.fetchone() if entry is None: return APIResponse( status=404, error="Entry with specified ID does not exist." ) return APIResponse( result=dict(entry) )
async def update_context() -> dict: async with app.acquire_db() as con: await con.execute(""" SELECT COUNT(*) FROM shows; """) shows = await con.fetchval() await con.execute(""" SELECT COUNT(*) FROM show_entry; """) entries = await con.fetchval() stats = { "shows": shows, "entries": entries, "seen": len(app.seen_titles), "version": version } return { "stats": stats, "updates": app.update_info, "docker": os.environ.get("IS_DOCKER", False) }
async def nyaa_search() -> str: ctx = {} resources = [ "base" ] fluent = get_injector(resources) ctx["_"] = fluent.format_value async with app.acquire_db() as con: await con.execute(""" SELECT id, title FROM shows ORDER BY title; """) shows = await con.fetchall() ctx["shows"] = [dict(s) for s in shows] ctx["seen_titles"] = list(app.seen_titles) return await render_template("nyaa_search.html", **ctx)
async def ensure_auth() -> Optional[APIResponse]: authed = False if request.headers.get("Authorization"): token = request.headers["Authorization"] async with app.acquire_db() as con: try: await con.execute( """ SELECT id FROM users WHERE api_key=?; """, token) user = await con.fetchval() if user: authed = True except Exception: pass if not authed and await current_user.is_authenticated: authed = True if not authed: return APIResponse( status=401, result="You are not authorized to access this resource.") return None
async def post(self) -> APIResponse: """ Adds a search result to Tsundoku. .. :quickref: Nyaa; Add search result :status 200: task performed succesfully :status 400: invalid parameters :status 404: show id passed not found :form integer show_id: The show to add the result to. :form string torrent_link: A comma-separated list of webhook triggers. :returns: :class:`dict` """ arguments = await request.get_json() show_id = arguments.get("show_id") torrent_link = arguments.get("torrent_link") overwrite = arguments.get("overwrite") if not show_id or not torrent_link: return APIResponse(status=400, error="Missing required parameters.") try: show_id = int(show_id) except ValueError: return APIResponse(status=400, error="Show ID passed is not an integer.") async with app.acquire_db() as con: await con.execute( """ SELECT id FROM shows WHERE id=?; """, show_id) show_id = await con.fetchval() if not show_id: return APIResponse(status=404, error="Show ID does not exist in the database.") search_result = SearchResult.from_necessary(app, show_id, torrent_link) logger.info(f"Processing new search result for Show <s{show_id}>") entries = await search_result.process(overwrite=overwrite) return APIResponse(result=[e.to_dict() for e in entries])
async def config_token() -> APIResponse: api_key = request.headers.get( "Authorization") or await current_user.api_key if request.method == "POST": async with app.acquire_db() as con: new_key = str(uuid4()) await con.execute( """ UPDATE users SET api_key = ? WHERE api_key = ?; """, new_key, api_key) else: new_key = api_key return APIResponse(status=200, result=str(new_key))
async def delete(self, show_id: int) -> APIResponse: """ Deletes a show with the specified ID. This will delete all entries of that show as well. .. :quickref: Shows; Delete a show. :status 200: deletion request received :returns: :class:`bool` """ async with app.acquire_db() as con: await con.execute( """ DELETE FROM shows WHERE id=?; """, show_id) logger.info(f"Show Deleted - <s{show_id}>") return APIResponse(result=True)
async def delete(self, show_id: int, entry_id: int) -> APIResponse: """ Deletes a single entry from a show. .. :quickref: Entries; Delete an entry. :status 200: entry successfully deleted :status 404: entry with passed id not found :returns: :class:`bool` """ async with app.acquire_db() as con: await con.execute(""" DELETE FROM show_entry WHERE id=?; """, entry_id) logger.info(f"Entry Deleted - <e{entry_id}>") return APIResponse( result=True )
async def post(self, show_id: None) -> APIResponse: """ Adds a new show to the database. .. :quickref: Shows; Add a new show. :status 200: show added successfully :status 400: bad or missing arguments :status 500: unexpected server error :form string title: the new show's title :form string desired_format: the new show's desired file format :form string desired_folder: the new show's target folder for moving :form integer season: the season to use when naming the show :form integer episode_offset: the episode offset to use when renaming (default :code:`0`) :returns: :class:`dict` """ # show_id here will always be None. Having it as a parameter # is required due to how the defaults are handled with GET # and POST methods on the routing table. arguments = await request.get_json() # patch for ajax call if not arguments: await request.get_data() arguments = await request.form desired_format = arguments.get("desired_format") desired_folder = arguments.get("desired_folder") season = arguments.get("season") if season is None: return APIResponse(status=400, error="Missing season argument.") try: season = int(season) except ValueError: return APIResponse(status=400, error="Season is not an integer.") episode_offset = arguments.get("episode_offset") if episode_offset is None: episode_offset = 0 else: try: episode_offset = int(episode_offset) except ValueError: return APIResponse(status=400, error="Episode offset is not an integer.") show = await Show.insert(title=arguments["title"], desired_format=desired_format, desired_folder=desired_folder, season=season, episode_offset=episode_offset, watch=arguments.get("watch", True)) async with app.acquire_db() as con: await con.execute( """ INSERT OR IGNORE INTO webhook (show_id, base) SELECT (?), id FROM webhook_base; """, show.id_) logger.info( f"New Show Added, <s{show.id_}> - Preparing to Check for New Releases" ) app.poller.reset_rss_cache() await app.poller.poll() show = await Show.from_id(show.id_) return APIResponse(result=show.to_dict())
async def post(self, show_id: int, entry_id: int = None) -> APIResponse: """ Manually begins handling of an entry for a specified show. Handling involves downloading, moving, and renaming. If an empty string is passed for a magnet URL, nothing will be downloaded and the entry will be marked as complete. .. :quickref: Entries; Add an entry. :status 200: entry added successfully :status 400: invalid arguments :form integer episode: the entry's episode :form string magnet: the entry's magnet url :returns: :class:`dict` """ arguments = await request.get_json() required_arguments = {"episode", "magnet"} if all(arg not in arguments.keys() for arg in required_arguments): return APIResponse( status=400, error="Too many arguments or missing required argument." ) try: episode = int(arguments["episode"]) except ValueError: return APIResponse( status=400, error="Episode argument must be an integer." ) if arguments["magnet"]: magnet = await app.dl_client.get_magnet(arguments["magnet"]) entry_id = await app.downloader.begin_handling(show_id, episode, magnet) else: async with app.acquire_db() as con: await con.execute(""" INSERT INTO show_entry ( show_id, episode, current_state, torrent_hash ) VALUES (?, ?, ?, ?); """, show_id, episode, "completed", "") entry_id = con.lastrowid async with app.acquire_db() as con: await con.execute(""" SELECT id, show_id, episode, current_state, torrent_hash, file_path, last_update FROM show_entry WHERE id=?; """, entry_id) new_entry = await con.fetchone() entry = Entry(app, new_entry) logger.info(f"Entry Manually Added - <e{entry.id}>") await entry._handle_webhooks() return APIResponse( result=entry.to_dict() )