Skip to content

Commit 969cb64

Browse files
neckarosclaude
andcommitted
refactor: replace GroupMediaDownload with RsGroupDownload from shared interfaces
BREAKING CHANGE: The /libraries/:id/medias/download endpoint now expects RsGroupDownload instead of GroupMediaDownload<MediaDownloadUrl>. ## What changed - Removed `GroupMediaDownload<T>` and `MediaDownloadUrl` structs - Now uses `RsGroupDownload` and `RsRequest` from rs-plugin-common-interfaces - Added new lookup fields: `tags_lookup`, `people_lookup`, `albums_lookup` (separate from direct ID fields `tags`, `people`, `albums`) - Bumped rs-plugin-common-interfaces to 0.20.4 ## Migration Guide ### Before (TypeScript) ```typescript interface MediaDownloadUrl { url: string; parse: boolean; uploadId?: string; ignoreOriginDuplicate?: boolean; kind?: 'photo' | 'video' | 'archive' | 'album' | 'other'; filename?: string; mime?: string; description?: string; length?: number; thumbnailUrl?: string; peopleLookup?: string[]; seriesLookup?: string[]; tagsLookup?: string[]; season?: number; episode?: number; } interface GroupMediaDownload { group?: boolean; groupThumbnailUrl?: string; groupFilename?: string; groupMime?: string; files: MediaDownloadUrl[]; referer?: string; headers?: string[]; // "Name:Value" format cookies?: string[]; originUrl?: string; title?: string; ignoreOriginDuplicate?: boolean; description?: string; peopleLookup?: string[]; seriesLookup?: string[]; tagsLookup?: string[]; season?: number; episode?: number; } // Old usage const download: GroupMediaDownload = { files: [{ url: 'https://example.com/image.jpg', parse: false, peopleLookup: ['John Doe'], tagsLookup: ['vacation'] }], originUrl: 'https://example.com/post/123' }; ``` ### After (TypeScript) ```typescript interface RsRequest { url: string; uploadId?: string; mime?: string; size?: number; filename?: string; status?: 'unprocessed' | 'needParsing' | 'finalPublic' | 'finalPrivate'; referer?: string; headers?: [string, string][]; // Tuple array format cookies?: RsCookie[]; description?: string; ignoreOriginDuplicate?: boolean; // New lookup fields (text to search in DB) tagsLookup?: string[]; peopleLookup?: string[]; albumsLookup?: string[]; // renamed from seriesLookup // New download fields thumbnailUrl?: string; originUrl?: string; title?: string; kind?: 'photo' | 'video' | 'archive' | 'album' | 'other'; season?: number; episode?: number; } interface RsGroupDownload { group: boolean; // Now required, not optional groupThumbnailUrl?: string; groupFilename?: string; groupMime?: string; requests: RsRequest[]; // renamed from 'files' } // New usage - info moved to individual requests const download: RsGroupDownload = { group: false, requests: [{ url: 'https://example.com/image.jpg', status: 'unprocessed', // replaces parse: false peopleLookup: ['John Doe'], tagsLookup: ['vacation'], originUrl: 'https://example.com/post/123' // moved to request }] }; // For grouped downloads const groupedDownload: RsGroupDownload = { group: true, groupFilename: 'my-album.zip', groupThumbnailUrl: 'https://example.com/thumb.jpg', requests: [ { url: 'https://example.com/1.jpg', status: 'unprocessed' }, { url: 'https://example.com/2.jpg', status: 'unprocessed' } ] }; ``` ## Key differences 1. `files` renamed to `requests` 2. `parse: true` becomes `status: 'needParsing'` 3. `parse: false` becomes `status: 'unprocessed'` 4. `seriesLookup` renamed to `albumsLookup` 5. Group-level `originUrl`, `referer`, `headers`, `cookies` moved to individual requests 6. `headers` format changed from `["Name:Value"]` to `[["Name", "Value"]]` 7. `cookies` now uses structured `RsCookie` objects instead of strings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0e25a75 commit 969cb64

5 files changed

Lines changed: 60 additions & 203 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ sha256 = "1.5.0"
7474
axum-extra = { version = "0.9.2", features = ["query"] }
7575
http = "1.1.0"
7676
extism = "1.10.0"
77-
rs-plugin-common-interfaces = { version = "0.20.3", features = ["rusqlite",] }
77+
rs-plugin-common-interfaces = { version = "0.20.4", features = ["rusqlite",] }
7878
async-recursion = "1.1.0"
7979
async-compression = { version = "0.4.6", features = ["tokio"] }
8080
youtube_dl = { version = "0.10.0", features = ["tokio", "downloader-rustls-tls"] }

src/domain/media.rs

Lines changed: 2 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -440,139 +440,6 @@ impl MediaForAdd {
440440
}
441441
}
442442

443-
#[derive(Debug, Serialize, Deserialize, Clone)]
444-
#[serde(rename_all = "camelCase")]
445-
pub struct GroupMediaDownload<T> {
446-
pub group: Option<bool>,
447-
pub group_thumbnail_url: Option<String>,
448-
pub group_filename: Option<String>,
449-
pub group_mime: Option<String>,
450-
pub files: Vec<T>,
451-
452-
pub referer: Option<String>,
453-
pub headers: Option<Vec<String>>,
454-
pub cookies: Option<Vec<String>>,
455-
pub origin_url: Option<String>,
456-
457-
pub title: Option<String>,
458-
459-
pub ignore_origin_duplicate: Option<bool>,
460-
461-
pub description: Option<String>,
462-
pub people_lookup: Option<Vec<String>>,
463-
pub series_lookup: Option<Vec<String>>,
464-
pub tags_lookup: Option<Vec<String>>,
465-
pub season: Option<u32>,
466-
pub episode: Option<u32>,
467-
}
468-
impl<T> GroupMediaDownload<T> {
469-
pub fn headers_as_tuple(&self) -> Option<Vec<(String, String)>> {
470-
if let Some(headers) = &self.headers {
471-
headers
472-
.iter()
473-
.map(|v| {
474-
let splitted: Vec<&str> = v.split(':').collect();
475-
let result =
476-
if let (Some(name), Some(value)) = (splitted.get(0), splitted.get(1)) {
477-
Some((name.to_string(), value.to_string()))
478-
} else {
479-
None
480-
};
481-
return result;
482-
})
483-
.collect()
484-
} else {
485-
None
486-
}
487-
}
488-
}
489-
490-
#[derive(Debug, Serialize, Deserialize, Clone)]
491-
#[serde(rename_all = "camelCase")]
492-
pub struct MediaDownloadUrl {
493-
pub url: String,
494-
pub parse: bool,
495-
pub upload_id: Option<String>,
496-
497-
#[serde(default)]
498-
pub ignore_origin_duplicate: bool,
499-
//pub infos: Option<MediaForUpdate>,
500-
pub kind: Option<FileType>,
501-
pub filename: Option<String>,
502-
pub mime: Option<String>,
503-
pub description: Option<String>,
504-
pub length: Option<u64>,
505-
pub thumbnail_url: Option<String>,
506-
507-
pub people_lookup: Option<Vec<String>>,
508-
pub series_lookup: Option<Vec<String>>,
509-
pub tags_lookup: Option<Vec<String>>,
510-
pub season: Option<i32>,
511-
pub episode: Option<i32>,
512-
}
513-
514-
impl From<MediaDownloadUrl> for RsRequest {
515-
fn from(value: MediaDownloadUrl) -> Self {
516-
RsRequest {
517-
url: value.url,
518-
mime: value.mime,
519-
size: None,
520-
filename: value.filename,
521-
status: if value.parse {
522-
RsRequestStatus::NeedParsing
523-
} else {
524-
RsRequestStatus::Unprocessed
525-
},
526-
headers: None,
527-
cookies: None,
528-
files: None,
529-
selected_file: None,
530-
tags: value.tags_lookup,
531-
albums: value.series_lookup,
532-
people: value.people_lookup,
533-
ignore_origin_duplicate: value.ignore_origin_duplicate,
534-
..Default::default()
535-
}
536-
}
537-
}
538-
539-
impl From<GroupMediaDownload<MediaDownloadUrl>> for Vec<RsRequest> {
540-
fn from(value: GroupMediaDownload<MediaDownloadUrl>) -> Self {
541-
let headers = value.headers_as_tuple();
542-
//println!("Headers parsed {:?}", headers);
543-
let mut output = Vec::new();
544-
for file in value.files {
545-
output.push(RsRequest {
546-
upload_id: file.upload_id,
547-
url: file.url,
548-
mime: None,
549-
size: None,
550-
filename: file.filename,
551-
status: if file.parse {
552-
RsRequestStatus::NeedParsing
553-
} else {
554-
RsRequestStatus::Unprocessed
555-
},
556-
headers: headers.clone(),
557-
cookies: value
558-
.cookies
559-
.as_ref()
560-
.and_then(|c| c.iter().map(|s| RsCookie::from_str(s).ok()).collect()),
561-
files: None,
562-
selected_file: None,
563-
referer: value.referer.clone(),
564-
tags: file.tags_lookup.or(value.tags_lookup.clone()),
565-
albums: file.series_lookup.or(value.series_lookup.clone()),
566-
people: file.people_lookup.or(value.people_lookup.clone()),
567-
description: file.description.or(value.title.clone()),
568-
ignore_origin_duplicate: file.ignore_origin_duplicate,
569-
..Default::default()
570-
});
571-
}
572-
output
573-
}
574-
}
575-
576443
impl From<Media> for MediaForAdd {
577444
fn from(value: Media) -> Self {
578445
MediaForAdd {
@@ -621,61 +488,15 @@ impl From<RsRequest> for MediaForUpdate {
621488
name: value.filename_or_extract_from_url(),
622489
description: value.description,
623490
ignore_origin_duplicate: value.ignore_origin_duplicate,
624-
//kind: value.k,
625491
size: value.size,
626-
people_lookup: value.people,
627-
tags_lookup: value.tags,
628-
series_lookup: value.albums,
629-
..Default::default()
630-
}
631-
}
632-
}
633-
634-
impl From<GroupMediaDownload<MediaDownloadUrl>> for MediaForUpdate {
635-
fn from(value: GroupMediaDownload<MediaDownloadUrl>) -> Self {
636-
let mut update = MediaForUpdate {
637-
name: value.group_filename,
638-
description: value.description,
639-
ignore_origin_duplicate: value.ignore_origin_duplicate.unwrap_or_default(),
492+
// Use the new lookup fields for database text search
640493
people_lookup: value.people_lookup,
641494
tags_lookup: value.tags_lookup,
642-
series_lookup: value.series_lookup,
495+
series_lookup: value.albums_lookup,
643496
season: value.season,
644497
episode: value.episode,
645498
..Default::default()
646-
};
647-
648-
if value.group.unwrap_or_default() {
649-
let mut people_final = update.people_lookup.clone().unwrap_or_default();
650-
for file in &value.files {
651-
if let Some(people) = &file.people_lookup {
652-
for person in people {
653-
if !people_final.contains(person) {
654-
people_final.push(person.to_string());
655-
}
656-
}
657-
}
658-
}
659-
if !people_final.is_empty() {
660-
update.people_lookup = Some(people_final);
661-
}
662-
663-
let mut tags_final = update.tags_lookup.clone().unwrap_or_default();
664-
for file in &value.files {
665-
if let Some(tags) = &file.tags_lookup {
666-
for tag in tags {
667-
if !tags_final.contains(tag) {
668-
tags_final.push(tag.to_string());
669-
}
670-
}
671-
}
672-
}
673-
if !tags_final.is_empty() {
674-
update.tags_lookup = Some(tags_final);
675-
}
676499
}
677-
678-
update
679500
}
680501
}
681502

src/model/medias.rs

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use mime_guess::get_mime_extensions_str;
1313
use nanoid::nanoid;
1414
use query_external_ip::SourceError;
1515
use regex::Regex;
16-
use rs_plugin_common_interfaces::{request::{RsRequest, RsRequestStatus}, url::{RsLink, RsLinkType}, video::{RsVideoTranscodeJob, RsVideoTranscodeJobPluginRequest, RsVideoTranscodeStatus, VideoConvertRequest, VideoOverlayType}, PluginType, RsCookie};
16+
use rs_plugin_common_interfaces::{request::{RsGroupDownload, RsRequest, RsRequestStatus}, url::{RsLink, RsLinkType}, video::{RsVideoTranscodeJob, RsVideoTranscodeJobPluginRequest, RsVideoTranscodeStatus, VideoConvertRequest, VideoOverlayType}, PluginType, RsCookie};
1717
use rusqlite::{types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}, ToSql};
1818
use serde::{Deserialize, Serialize};
1919
use strum_macros::EnumString;
@@ -24,7 +24,7 @@ use zip::ZipWriter;
2424

2525
use crate::{domain::{MediaElement, deleted::RsDeleted, library::LibraryType, media::{self, ConvertMessage, ConvertProgress, DEFAULT_MIME, RsGpsPosition}, plugin}, error::RsError, model::store::sql::SqlOrder, plugins::sources::{Source, path_provider::PathProvider}, routes::infos, server::get_config, tools::{file_tools::{filename_from_path, remove_extension}, image_tools::{IMAGES_MIME_FULL_BROWSER_SUPPORT, convert_image_reader, image_infos}, recognition, video_tools::VideoCommandBuilder}};
2626

27-
use crate::{domain::{library::LibraryRole, media::{FileType, GroupMediaDownload, Media, MediaDownloadUrl, MediaForAdd, MediaForInsert, MediaForUpdate, MediaItemReference, MediaWithAction, MediasMessage, ProgressMessage}, progress::{RsProgress, RsProgressType}, ElementAction}, error::RsResult, plugins::{get_plugin_fodler, sources::{async_reader_progress::ProgressReader, error::SourcesError, AsyncReadPinBox, FileStreamResult, SourceRead}}, routes::mw_range::RangeDefinition, server::get_server_port, tools::{auth::{sign_local, ClaimsLocal}, file_tools::{file_type_from_mime, get_extension_from_mime}, image_tools::{self, resize_image_reader, ImageSize}, log::{log_error, log_warn, log_info, LogServiceType}, prediction::{predict_net, preload_model, PredictionTagResult}, video_tools::{self, probe_video, VideoTime}}};
27+
use crate::{domain::{library::LibraryRole, media::{FileType, Media, MediaForAdd, MediaForInsert, MediaForUpdate, MediaItemReference, MediaWithAction, MediasMessage, ProgressMessage}, progress::{RsProgress, RsProgressType}, ElementAction}, error::RsResult, plugins::{get_plugin_fodler, sources::{async_reader_progress::ProgressReader, error::SourcesError, AsyncReadPinBox, FileStreamResult, SourceRead}}, routes::mw_range::RangeDefinition, server::get_server_port, tools::{auth::{sign_local, ClaimsLocal}, file_tools::{file_type_from_mime, get_extension_from_mime}, image_tools::{self, resize_image_reader, ImageSize}, log::{log_error, log_warn, log_info, LogServiceType}, prediction::{predict_net, preload_model, PredictionTagResult}, video_tools::{self, probe_video, VideoTime}}};
2828

2929
use super::{error::{Error, Result}, plugins::PluginQuery, store::{self, sql::library::medias::MediaBackup}, users::ConnectedUser, ModelController, VideoConvertQueueElement};
3030
use crate::routes::sse::SseEvent;
@@ -180,6 +180,28 @@ impl TryFrom<Media> for MediaSource {
180180

181181
impl ModelController {
182182

183+
/// Helper to aggregate lookup fields from multiple requests into a single deduplicated list
184+
fn aggregate_lookup_field<F>(requests: &[RsRequest], getter: F) -> Option<Vec<String>>
185+
where
186+
F: Fn(&RsRequest) -> Option<Vec<String>>,
187+
{
188+
let mut result: Vec<String> = Vec::new();
189+
for request in requests {
190+
if let Some(values) = getter(request) {
191+
for value in values {
192+
if !result.contains(&value) {
193+
result.push(value);
194+
}
195+
}
196+
}
197+
}
198+
if result.is_empty() {
199+
None
200+
} else {
201+
Some(result)
202+
}
203+
}
204+
183205
pub async fn get_medias(&self, library_id: &str, query: MediaQuery, requesting_user: &ConnectedUser) -> RsResult<Vec<Media>> {
184206
let progress_user = self.get_library_mapped_user(library_id, requesting_user.user_id()?).await.ok();
185207
let mut limits = requesting_user.check_library_role(library_id, LibraryRole::Read)?;
@@ -655,36 +677,52 @@ impl ModelController {
655677
}
656678

657679

658-
pub async fn download_library_url(&self, library_id: &str, files: GroupMediaDownload<MediaDownloadUrl>, requesting_user: &ConnectedUser) -> RsResult<Vec<Media>> {
680+
pub async fn download_library_url(&self, library_id: &str, files: RsGroupDownload, requesting_user: &ConnectedUser) -> RsResult<Vec<Media>> {
659681

660682
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
661683
self.cache_check_library_notcrypt(library_id).await?;
662684

663685
let m = self.source_for_library(library_id).await?;
664686
let store = self.store.get_library_store(library_id)?;
665687
let mut medias: Vec<Media> = vec![];
666-
let origin = if let Some(origin) = &files.origin_url {
688+
689+
// Get group-level info from the first request
690+
let first_request = files.requests.first();
691+
let origin_url = first_request.and_then(|r| r.origin_url.clone());
692+
let origin = if let Some(origin) = &origin_url {
667693
Some(self.exec_parse(Some(library_id.to_owned()), origin.to_owned(), requesting_user).await.ok().unwrap_or(RsLink {platform: "link".to_owned(), kind: Some(RsLinkType::Post), id: origin.to_owned(), ..Default::default()}))
668694
} else {
669695
None
670696
};
671697

672698

673-
if files.group.unwrap_or_default() {
699+
if files.group {
674700
let thumbnail = files.group_thumbnail_url.clone();
675-
let mut infos: MediaForUpdate = files.clone().into();
701+
// Build MediaForUpdate from group data and first request
702+
let mut infos = MediaForUpdate {
703+
name: files.group_filename.clone(),
704+
mimetype: files.group_mime.clone(),
705+
// Aggregate lookup fields from all requests
706+
people_lookup: Self::aggregate_lookup_field(&files.requests, |r| r.people_lookup.clone()),
707+
tags_lookup: Self::aggregate_lookup_field(&files.requests, |r| r.tags_lookup.clone()),
708+
series_lookup: Self::aggregate_lookup_field(&files.requests, |r| r.albums_lookup.clone()),
709+
ignore_origin_duplicate: first_request.map(|r| r.ignore_origin_duplicate).unwrap_or_default(),
710+
season: first_request.and_then(|r| r.season),
711+
episode: first_request.and_then(|r| r.episode),
712+
..Default::default()
713+
};
676714
infos.origin = origin.clone();
677715

678716
let mut medias: Vec<Media> = vec![];
679-
let requests: Vec<RsRequest> = files.clone().into();
717+
let requests = files.requests.clone();
680718

681719
let upload_id = nanoid!();
682720
//Progress
683721
let tx_progress = self.create_progress_sender(library_id.to_owned(), Some(upload_id.clone()));
684722

685723

686724
if let Some(origin) = &mut infos.origin {
687-
let origin_filename = if let Some(origin_url) = files.origin_url.clone() { filename_from_path(&origin_url) } else { None };
725+
let origin_filename = if let Some(origin_url) = origin_url.clone() { filename_from_path(&origin_url) } else { None };
688726
origin.file = origin_filename;
689727
if !infos.ignore_origin_duplicate {
690728
let existing = store.get_media_by_origin(origin.clone()).await;
@@ -771,19 +809,19 @@ impl ModelController {
771809

772810
let mut loaded_thumb = false;
773811
if let Some(thumbUrl) = thumbnail {
774-
let headers = files.headers_as_tuple();
812+
// Get headers, cookies, and referer from first request
775813
let request = RsRequest {
776814
upload_id: None,
777815
url: thumbUrl,
778816
mime: None,
779817
size: None,
780818
filename: None,
781819
status: RsRequestStatus::Unprocessed,
782-
headers: headers.clone(),
783-
cookies: files.cookies.as_ref().and_then(|c| c.iter().map(|s| RsCookie::from_str(s).ok()).collect()),
820+
headers: first_request.and_then(|r| r.headers.clone()),
821+
cookies: first_request.and_then(|r| r.cookies.clone()),
784822
files: None,
785823
selected_file: None,
786-
referer: files.referer.clone(),
824+
referer: first_request.and_then(|r| r.referer.clone()),
787825
..Default::default()
788826
};
789827

@@ -819,10 +857,8 @@ impl ModelController {
819857

820858
Ok(medias)
821859
} else {
822-
let requests: Vec<RsRequest> = files.into();
823-
824-
//let infos = infos.unwrap_or_else(|| MediaForUpdate::default());
825-
for mut request in requests {
860+
// Non-grouped path - process each request individually
861+
for mut request in files.requests {
826862
let upload_id = request.upload_id.clone().unwrap_or_else(|| nanoid!());
827863

828864
self.plugin_manager.fill_infos(&mut request).await;

src/routes/medias.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::{mw_range::RangeDefinition, ImageRequestOptions, ImageUploadOptions};
44
use crate::{
55
domain::{
66
media::{
7-
self, GroupMediaDownload, MediaDownloadUrl, MediaForUpdate, MediaItemReference,
7+
self, MediaForUpdate, MediaItemReference,
88
MediaWithAction, MediasMessage,
99
},
1010
ElementAction,
@@ -35,7 +35,7 @@ use axum::{
3535
use axum_extra::extract::Query;
3636
use futures::TryStreamExt;
3737
use hyper::{header::ACCEPT_RANGES, StatusCode};
38-
use rs_plugin_common_interfaces::{request::RsRequest, video::VideoConvertRequest};
38+
use rs_plugin_common_interfaces::{request::{RsGroupDownload, RsRequest}, video::VideoConvertRequest};
3939
use serde::{Deserialize, Serialize};
4040
use serde_json::{json, Value};
4141
use tokio::io::{AsyncRead, AsyncReadExt};
@@ -559,7 +559,7 @@ async fn handler_download(
559559
State(mc): State<ModelController>,
560560
user: ConnectedUser,
561561
Query(query): Query<UploadOption>,
562-
Json(download): Json<GroupMediaDownload<MediaDownloadUrl>>,
562+
Json(download): Json<RsGroupDownload>,
563563
) -> Result<Json<Value>> {
564564
if query.spawn {
565565
tokio::spawn(async move {

0 commit comments

Comments
 (0)