From dca70ba2cc9bca925280c8930c3f4b3fc98a7d2c Mon Sep 17 00:00:00 2001 From: shiroyashik Date: Sun, 9 Feb 2025 18:58:24 +0300 Subject: [PATCH] Bump version to 0.2.2, add inline command for listing unviewed videos --- Cargo.lock | 2 +- Cargo.toml | 2 +- Dockerfile | 2 +- src/handle/list.rs | 207 +++++++++++++++++++++++++++++++-------------- src/handle/mod.rs | 1 + src/inline.rs | 2 + 6 files changed, 148 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0f6449..b02a08c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "doggy-watch" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index d2fc490..42d6df5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "doggy-watch" authors = ["Shiroyashik "] -version = "0.2.0" +version = "0.2.2" edition = "2021" publish = false diff --git a/Dockerfile b/Dockerfile index a743db1..b966d6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ## Chef # FROM clux/muslrust:stable AS chef -FROM rust:1.84.0-alpine3.20 AS chef +FROM rust:alpine AS chef USER root RUN apk add --no-cache musl-dev libressl-dev RUN cargo install cargo-chef diff --git a/src/handle/list.rs b/src/handle/list.rs index 3e14f59..6a14ee6 100644 --- a/src/handle/list.rs +++ b/src/handle/list.rs @@ -1,84 +1,161 @@ use std::sync::Arc; use indexmap::IndexMap; -use teloxide::{prelude::*, types::{LinkPreviewOptions, ParseMode}}; +use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions, ParseMode}}; use sea_orm::{prelude::*, Order, QueryOrder}; use database::*; use crate::AppState; +struct Video { + id: i32, + title: String, + url: String, + contributors: u64, + status: String, +} + pub async fn command(bot: Bot, msg: Message, state: Arc) -> anyhow::Result<()> { - struct Video { - id: i32, - title: String, - url: String, - contributors: u64, - status: String, - } let videos: Vec<(requests::Model, Option)> = requests::Entity::find() .find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).all(&state.db).await?; - // let videos_len = videos.len(); - if !videos.is_empty() { - let mut by_date: IndexMap> = IndexMap::new(); - for (request, video) in videos { - let video = video.unwrap(); - let creator = if let Some(c) = request.find_related(actions::Entity).order_by(actions::Column::Id, Order::Asc).one(&state.db).await? { - c + + let result = generate_list(videos, &state).await; + match result { + Ok(list) => { + let result = if let Some(list) = list { + list } else { - let data = format!("Can't find creator for {request:?}"); - bot.send_message(msg.chat.id, data.clone()).await?; - anyhow::bail!(data); + "Нет видео для просмотра :(".to_string() }; - let contributors = request.find_related(actions::Entity).count(&state.db).await?; - let date = creator.created_at.date(); - let url = format!("{}{}", youtube::DEFAULT_YT, video.ytid); + let keyboard: Vec> = vec![ + vec![InlineKeyboardButton::callback("Непросмотренные", "list_unviewed")], + ]; - let viewed_times = archived::Entity::find().filter(archived::Column::Ytid.eq(video.ytid.clone())).filter(archived::Column::ViewedAt.is_not_null()).count(&state.db).await?; - let archived_times = archived::Entity::find().filter(archived::Column::Ytid.eq(video.ytid)).count(&state.db).await?; - - let mut status = String::new(); - status.push(if request.viewed_at.is_some() { - '👀' - } else if viewed_times != 0 { - '⭐' - } else if archived_times != 0 { - '📁' - } else { - '🆕' - }); - - if let Some(entry) = by_date.get_mut(&date) { - entry.push(Video { id: request.id, title: video.title, url, contributors, status }); - } else { - by_date.insert(date, vec![Video { id: request.id, title: video.title, url, contributors, status }]); - }; - } - by_date.sort_unstable_by(|a, _, c, _| c.cmp(a)); - let mut result = String::new(); - for (date, mut videos) in by_date { - if result.is_empty() { - result.push_str(&format!("[{}]", date.format("%d.%m"))); - } else { - result.push_str(&format!("\n[{}]", date.format("%d.%m"))); - } - // result.push_str(&format!(" {}", videos.len())); - videos.sort_unstable_by(|a, b| a.contributors.cmp(&b.contributors)); - for video in videos { - let contributors = if video.contributors != 1 { - format!("(🙍‍♂️{}) ", video.contributors) - } else { - String::new() - }; - result.push_str(&format!("\n{}/{} 📺YT {}{}", video.status, video.id, video.url, contributors, video.title)); - // result.push_str(&format!("\n{}. {} YT ({})", me.username.clone().unwrap(), video.id, video.id, video.title, video.url, video.contributors)); - } - } - // result.push_str(&format!("\nВсего: {}", videos_len)); - bot.send_message(msg.chat.id, result).parse_mode(ParseMode::Html).link_preview_options(LinkPreviewOptions { is_disabled: true, url: None, prefer_small_media: false, prefer_large_media: false, show_above_text: false }).await?; - } else { - bot.send_message(msg.chat.id, "Нет видео для просмотра :(").await?; + bot.send_message(msg.chat.id, result).parse_mode(ParseMode::Html) + .link_preview_options(LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false + }).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; + }, + Err(e) => { + tracing::error!("{:?}", e); + bot.send_message(msg.chat.id, "Произошла ошибка!").await?; + }, } Ok(()) +} + +pub async fn inline(state: Arc, bot: Bot, q: CallbackQuery) -> anyhow::Result<()> { + bot.answer_callback_query(&q.id).await?; + let videos: Vec<(requests::Model, Option)> = requests::Entity::find() + .find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).filter(requests::Column::ViewedAt.is_null()).all(&state.db).await?; + let result = generate_list(videos, &state).await; + match result { + Ok(list) => { + let result = if let Some(list) = list { + list + } else { + "Нет видео для просмотра :(".to_string() + }; + + let keyboard: Vec> = vec![ + vec![InlineKeyboardButton::callback("Обновить", "list_unviewed")], + ]; + + if let Some(message) = q.regular_message() { + bot.edit_message_text(message.chat.id, message.id, result).parse_mode(ParseMode::Html) + .link_preview_options(LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false + }).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; + } else if let Some(message_id) = q.inline_message_id { + bot.edit_message_text_inline(&message_id, result) + .parse_mode(ParseMode::Html).disable_web_page_preview(true).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; + } else { + bot.send_message(q.from.id, result).parse_mode(ParseMode::Html) + .reply_markup(InlineKeyboardMarkup::new(keyboard)) + .link_preview_options(LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false + }).await?; + } + }, + Err(e) => { + tracing::error!("{:?}", e); + bot.send_message(q.from.id, "Произошла ошибка!").await?; + }, + } + Ok(()) +} + +async fn generate_list(videos: Vec<(requests::Model, Option)>, state: &AppState) -> anyhow::Result> { + if videos.is_empty() { + return Ok(None); + } + let mut by_date: IndexMap> = IndexMap::new(); + for (request, video) in videos { + let video = video.unwrap(); + let creator = if let Some(c) = request.find_related(actions::Entity).order_by(actions::Column::Id, Order::Asc).one(&state.db).await? { + c + } else { + anyhow::bail!("Can't find creator for {request:?}"); + }; + + let contributors = request.find_related(actions::Entity).count(&state.db).await?; + let date = creator.created_at.date(); + let url = format!("{}{}", youtube::DEFAULT_YT, video.ytid); + + let viewed_times = archived::Entity::find().filter(archived::Column::Ytid.eq(video.ytid.clone())).filter(archived::Column::ViewedAt.is_not_null()).count(&state.db).await?; + let archived_times = archived::Entity::find().filter(archived::Column::Ytid.eq(video.ytid)).count(&state.db).await?; + + let mut status = String::new(); + status.push(if request.viewed_at.is_some() { + '👀' + } else if viewed_times != 0 { + '⭐' + } else if archived_times != 0 { + '📁' + } else { + '🆕' + }); + + if let Some(entry) = by_date.get_mut(&date) { + entry.push(Video { id: request.id, title: video.title, url, contributors, status }); + } else { + by_date.insert(date, vec![Video { id: request.id, title: video.title, url, contributors, status }]); + }; + } + by_date.sort_unstable_by(|a, _, c, _| c.cmp(a)); + let mut result = String::new(); + for (date, mut videos) in by_date { + if result.is_empty() { + result.push_str(&format!("[{}]", date.format("%d.%m"))); + } else { + result.push_str(&format!("\n[{}]", date.format("%d.%m"))); + } + // result.push_str(&format!(" {}", videos.len())); + videos.sort_unstable_by(|a, b| a.contributors.cmp(&b.contributors)); + for video in videos { + let contributors = if video.contributors != 1 { + format!("(🙍‍♂️{}) ", video.contributors) + } else { + String::new() + }; + result.push_str(&format!("\n{}/{} 📺YT {}{}", video.status, video.id, video.url, contributors, video.title)); + // result.push_str(&format!("\n{}. {} YT ({})", me.username.clone().unwrap(), video.id, video.id, video.title, video.url, video.contributors)); + } + } + // result.push_str(&format!("\nВсего: {}", videos_len)); + Ok(Some(result)) } \ No newline at end of file diff --git a/src/handle/mod.rs b/src/handle/mod.rs index 6e7b320..67f7cc4 100644 --- a/src/handle/mod.rs +++ b/src/handle/mod.rs @@ -74,6 +74,7 @@ pub fn schema() -> UpdateHandler { InlineCommand::parse(&q.data?) })) .branch(case![InlineCommand::Cancel].endpoint(cancel)) + .branch(case![InlineCommand::ListUnviewed].endpoint(list::inline)) .branch(filter(|com: InlineCommand| { matches!(com, InlineCommand::ArchiveAll | InlineCommand::ArchiveViewed) }).endpoint(archive::inline)) diff --git a/src/inline.rs b/src/inline.rs index 6642e34..3b31d91 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -6,6 +6,7 @@ pub enum InlineCommand { Unview(i32), ArchiveViewed, ArchiveAll, + ListUnviewed, Cancel, } @@ -19,6 +20,7 @@ impl InlineCommand { "unview" => Self::Unview(parts.next()?.parse().ok()?), "archive_viewed" => Self::ArchiveViewed, "archive_all" => Self::ArchiveAll, + "list_unviewed" => Self::ListUnviewed, "cancel" => Self::Cancel, _ => return None, })