diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e477f1f..d8b8c25 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,9 +17,14 @@ jobs: steps: - uses: actions/checkout@v3 + + - uses: dtolnay/rust-toolchain@stable + - name: Build run: cargo build --verbose + - name: Run Clippy - run: cargo clippy --all-targets --all-features + run: cargo clippy + - name: Run tests run: cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index f849a9a..375eb78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,7 +954,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "pmv-cli" -version = "2.0.0" +version = "3.0.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index e3ce936..039359c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ description = "Command line interface client for PersonalMediaVault" edition = "2021" license = "MIT" name = "pmv-cli" -version = "2.0.0" +version = "3.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/MANUAL.md b/MANUAL.md index bd39e55..dfedb25 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -24,6 +24,7 @@ pmv-cli [OPTIONS] | [config](#command-config) | Manages vault configuration | | [task](#command-task) | Retrieves tasks information | | [invites](#command-invites) | Manages invites | +| [home](#command-home) | Manges the home page | | [batch](#command-batch) | Applies a batch operation to a list of media assets | | [get-server-information](#command-get-server-information) | Gets server information, like the version it is using | | [get-disk-usage](#command-get-disk-usage) | Gets server disk usage | @@ -367,12 +368,13 @@ pmv-cli media | [get](#command-media-get) | Gets media asset metadata and download links | | [stats](#command-media-stats) | Gets media asset size stats | | [download](#command-media-download) | Downloads a media asset | +| [get-related](#command-media-get-related) | Gets related media | | [export](#command-media-export) | Exports a media asset, downloading everything (metadata + assets) into a folder | | [upload](#command-media-upload) | Uploads a new media asset, waits for encryption and adds tags if specified | | [import](#command-media-import) | Imports a media asset, expecting a folder with the same format the export command uses | | [set-title](#command-media-set-title) | Changes the title of a media asset | | [set-description](#command-media-set-description) | Changes the description of a media asset | -| [set-extended-description](#command-media-set-extended-description) | Changes the extended description of a media asset | +| [set-related-media](#command-media-set-related-media) | Sets the related media list | | [set-force-start-beginning](#command-media-set-force-start-beginning) | Changes the forced start from beginning parameter of a media asset | | [set-is-animation](#command-media-set-is-animation) | Changes the is-animation parameter of a media asset | | [set-thumbnail](#command-media-set-thumbnail) | Sets the thumbnail of a media asset | @@ -469,6 +471,30 @@ pmv-cli media download [OPTIONS] [ASSET] | `-p, --print-link` | Prints the download link, instead of downloading to a file | | `-h, --help` | Print help | +### Command: media get-related + +Gets related media + +**Usage:** + +``` +pmv-cli media get-related [OPTIONS] +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Media asset ID | + +**Options:** + +| Option | Description | +| --- | --- | +| `-e, --extended` | Extended version of the results table | +| `-c, --csv` | CSV format | +| `-h, --help` | Print help | + ### Command: media export Exports a media asset, downloading everything (metadata + assets) into a folder @@ -571,7 +597,7 @@ Changes the description of a media asset **Usage:** ``` -pmv-cli media set-description +pmv-cli media set-description ``` **Arguments:** @@ -579,7 +605,7 @@ pmv-cli media set-description | Argument | Description | | --- | --- | | `` | Media asset ID | -| `` | Description | +| `` | Path to the text file containing the description | **Options:** @@ -587,14 +613,14 @@ pmv-cli media set-description | --- | --- | | `-h, --help` | Print help | -### Command: media set-extended-description +### Command: media set-related-media -Changes the extended description of a media asset +Sets the related media list **Usage:** ``` -pmv-cli media set-extended-description +pmv-cli media set-related-media ``` **Arguments:** @@ -602,7 +628,7 @@ pmv-cli media set-extended-description | Argument | Description | | --- | --- | | `` | Media asset ID | -| `` | Path to the text file containing the extended description | +| `` | List of related media IDs, separated by commas | **Options:** @@ -1134,7 +1160,6 @@ pmv-cli advanced-search [OPTIONS] | Option | Description | | --- | --- | | `-q, --title ` | Filter by title | -| `-d, --description <DESCRIPTION>` | Filter by description | | `-k, --media-type <MEDIA_TYPE>` | Filter by media type. Can be: video, audio or image | | `-t, --tags <TAGS>` | Filter by tags. Expected a list of tag names, separated by spaces | | `-m, --tags-mode <TAGS_MODE>` | Tag filtering mode. Can be: all, any, none or untagged | @@ -2098,6 +2123,188 @@ pmv-cli invites close-session <INDEX> | --- | --- | | `-h, --help` | Print help | +## Command: home + +Manges the home page + +<ins>**Usage:**</ins> + +``` +pmv-cli home <COMMAND> +``` + +<ins>**Commands:**</ins> + +| Command | Description | +| --- | --- | +| [get-groups](#command-home-get-groups) | Gets the groups in the home page | +| [add-group](#command-home-add-group) | Adds a home page group | +| [get-group-elements](#command-home-get-group-elements) | Gets the elements of a group | +| [set-group-elements](#command-home-set-group-elements) | Sets the elements of a group | +| [rename-group](#command-home-rename-group) | Renames an existing group | +| [move-group](#command-home-move-group) | Moves an existing group to another position | +| [delete-group](#command-home-delete-group) | Deletes an existing group | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: home get-groups + +Gets the groups in the home page + +<ins>**Usage:**</ins> + +``` +pmv-cli home get-groups +``` + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: home add-group + +Adds a home page group + +<ins>**Usage:**</ins> + +``` +pmv-cli home add-group [OPTIONS] <NAME> <GROUP_TYPE> +``` + +<ins>**Arguments:**</ins> + +| Argument | Description | +| --- | --- | +| `<NAME>` | A name for the group | +| `<GROUP_TYPE>` | The type of group (CUSTOM, RECENT_MEDIA or RECENT_ALBUMS) | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `--prepend` | Add this option in order to add the group at the top of the list | +| `-h, --help` | Print help | + +### Command: home get-group-elements + +Gets the elements of a group + +<ins>**Usage:**</ins> + +``` +pmv-cli home get-group-elements [OPTIONS] <ID> +``` + +<ins>**Arguments:**</ins> + +| Argument | Description | +| --- | --- | +| `<ID>` | ID of the group | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `--as-refs` | Add this option to print the elements as references | +| `-h, --help` | Print help | + +### Command: home set-group-elements + +Sets the elements of a group + +<ins>**Usage:**</ins> + +``` +pmv-cli home set-group-elements <ID> <ELEMENTS> +``` + +<ins>**Arguments:**</ins> + +| Argument | Description | +| --- | --- | +| `<ID>` | ID of the group | +| `<ELEMENTS>` | List of group elements, as references, separated by commas. For media elements use M{ID} and for albums use A{ID}. Example: A12, M6, M8 | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: home rename-group + +Renames an existing group + +<ins>**Usage:**</ins> + +``` +pmv-cli home rename-group <ID> <NAME> +``` + +<ins>**Arguments:**</ins> + +| Argument | Description | +| --- | --- | +| `<ID>` | ID of the group | +| `<NAME>` | A name for the group | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: home move-group + +Moves an existing group to another position + +<ins>**Usage:**</ins> + +``` +pmv-cli home move-group <ID> <POSITION> +``` + +<ins>**Arguments:**</ins> + +| Argument | Description | +| --- | --- | +| `<ID>` | ID of the group | +| `<POSITION>` | The position to move the group | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: home delete-group + +Deletes an existing group + +<ins>**Usage:**</ins> + +``` +pmv-cli home delete-group <ID> +``` + +<ins>**Arguments:**</ins> + +| Argument | Description | +| --- | --- | +| `<ID>` | ID of the group | + +<ins>**Options:**</ins> + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + ## Command: batch Applies a batch operation to a list of media assets @@ -2123,7 +2330,6 @@ pmv-cli batch [OPTIONS] <COMMAND> | Option | Description | | --- | --- | | `-q, --title <TITLE>` | Filter by title | -| `-d, --description <DESCRIPTION>` | Filter by description | | `-k, --media-type <MEDIA_TYPE>` | Filter by media type. Can be: video, audio or image | | `-t, --tags <TAGS>` | Filter by tags. Expected a list of tag names, separated by spaces | | `-m, --tags-mode <TAGS_MODE>` | Tag filtering mode. Can be: all, any, none or untagged | diff --git a/src/api/home.rs b/src/api/home.rs new file mode 100644 index 0000000..aed4378 --- /dev/null +++ b/src/api/home.rs @@ -0,0 +1,132 @@ +// Home page API + +use crate::{ + models::{ + HomePageAddGroupBody, HomePageElement, HomePageGroup, HomePageGroupMoveBody, + HomePageGroupRenameBody, HomePageGroupSetElementsBody, + }, + tools::{do_delete_request, do_get_request, do_post_request, RequestError, VaultURI}, +}; + +pub async fn api_call_get_home_groups( + url: &VaultURI, + debug: bool, +) -> Result<Vec<HomePageGroup>, RequestError> { + let body_str = do_get_request(url, "/api/home".to_string(), debug).await?; + + let parsed_body: Result<Vec<HomePageGroup>, _> = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + +pub async fn api_call_home_add_group( + url: &VaultURI, + req_body: HomePageAddGroupBody, + debug: bool, +) -> Result<HomePageGroup, RequestError> { + let body_str = do_post_request( + url, + "/api/home".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + let parsed_body: Result<HomePageGroup, _> = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + +pub async fn api_call_get_home_group_elements( + url: &VaultURI, + id: u64, + debug: bool, +) -> Result<Vec<HomePageElement>, RequestError> { + let body_str = do_get_request(url, format!("/api/home/{}/elements", id), debug).await?; + + let parsed_body: Result<Vec<HomePageElement>, _> = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + +pub async fn api_call_set_home_group_elements( + url: &VaultURI, + id: u64, + req_body: HomePageGroupSetElementsBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + format!("/api/home/{}/elements", id), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_rename_home_group( + url: &VaultURI, + id: u64, + req_body: HomePageGroupRenameBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + format!("/api/home/{}/name", id), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_move_home_group( + url: &VaultURI, + id: u64, + req_body: HomePageGroupMoveBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + format!("/api/home/{}/move", id), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_delete_home_group( + url: &VaultURI, + id: u64, + debug: bool, +) -> Result<(), RequestError> { + do_delete_request(url, format!("/api/home/{}", id), debug).await?; + + Ok(()) +} diff --git a/src/api/media.rs b/src/api/media.rs index cf5609f..7597d94 100644 --- a/src/api/media.rs +++ b/src/api/media.rs @@ -4,10 +4,11 @@ use std::sync::{Arc, Mutex}; use crate::{ models::{ - ImageNote, MediaAssetSizeStats, MediaAttachment, MediaAudioTrack, MediaMetadata, MediaRenameAttachmentBody, MediaRenameSubtitleOrAudioBody, MediaResolution, MediaSubtitle, MediaTimeSlice, MediaUpdateDescriptionBody, MediaUpdateExtendedDescriptionBody, MediaUpdateExtraBody, MediaUpdateThumbnailResponse, MediaUpdateTitleBody, MediaUploadResponse, TaskEncodeResolution + ImageNote, MediaAssetSizeStats, MediaAttachment, MediaAudioTrack, MediaMetadata, MediaRenameAttachmentBody, MediaRenameSubtitleOrAudioBody, MediaResolution, MediaSubtitle, MediaTimeSlice, MediaUpdateDescriptionBody, MediaUpdateExtraBody, MediaUpdateRelatedMediaBody, MediaUpdateThumbnailResponse, MediaUpdateTitleBody, MediaUploadResponse, TaskEncodeResolution }, tools::{ - do_get_request, do_multipart_upload_request, do_multipart_upload_request_with_confirmation, do_post_request, ProgressReceiver, RequestError, VaultURI + do_get_request, do_multipart_upload_request, do_multipart_upload_request_with_confirmation, + do_post_request, ProgressReceiver, RequestError, VaultURI, }, }; @@ -132,7 +133,7 @@ pub async fn api_call_media_change_title( Ok(()) } -pub async fn api_call_media_change_description( +pub async fn api_call_media_change_extended_description( url: &VaultURI, media: u64, req_body: MediaUpdateDescriptionBody, @@ -149,23 +150,6 @@ pub async fn api_call_media_change_description( Ok(()) } -pub async fn api_call_media_change_extended_description( - url: &VaultURI, - media: u64, - req_body: MediaUpdateExtendedDescriptionBody, - debug: bool, -) -> Result<(), RequestError> { - do_post_request( - url, - format!("/api/media/{media}/edit/ext_desc"), - serde_json::to_string(&req_body).unwrap(), - debug, - ) - .await?; - - Ok(()) -} - pub async fn api_call_media_change_extra( url: &VaultURI, media: u64, @@ -388,7 +372,13 @@ pub async fn api_call_media_rename_subtitle( url_path.push_str(&("?id=".to_owned() + &urlencoding::encode(&sub_id))); - do_post_request(url, url_path, serde_json::to_string(&req_body).unwrap(), debug).await?; + do_post_request( + url, + url_path, + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; Ok(()) } @@ -455,7 +445,13 @@ pub async fn api_call_media_rename_audio( url_path.push_str(&("?id=".to_owned() + &urlencoding::encode(&audio_id))); - do_post_request(url, url_path, serde_json::to_string(&req_body).unwrap(), debug).await?; + do_post_request( + url, + url_path, + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; Ok(()) } @@ -537,3 +533,20 @@ pub async fn api_call_media_rename_attachment( Ok(()) } + +pub async fn api_call_media_change_related( + url: &VaultURI, + media: u64, + req_body: MediaUpdateRelatedMediaBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + format!("/api/media/{media}/edit/related"), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 988e5d0..c245458 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -29,3 +29,6 @@ pub use tags::*; mod tasks; pub use tasks::*; + +mod home; +pub use home::*; diff --git a/src/commands/album.rs b/src/commands/album.rs index 5d5fea2..72513fb 100644 --- a/src/commands/album.rs +++ b/src/commands/album.rs @@ -518,7 +518,7 @@ pub async fn run_cmd_get_album( } } else { println!( - "\"Pos\",\"Id\",\"Type\",\"Title\",\"Description\",\"Tags\",\"Duration\"" + "\"Pos\",\"Id\",\"Type\",\"Title\",\"Tags\",\"Duration\"" ); for (i, item) in album_data.list.iter().enumerate() { @@ -526,13 +526,12 @@ pub async fn run_cmd_get_album( let row_id = item.id.to_string(); let row_type = to_csv_string(&item.media_type.to_type_string()); let row_title = to_csv_string(&item.title); - let row_description = to_csv_string(&item.description); let row_tags = to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")); let row_duration = render_media_duration(item.media_type, item.duration.unwrap_or(0.0)); - println!("{row_pos},{row_id},{row_type},{row_title},{row_description},{row_tags},{row_duration}"); + println!("{row_pos},{row_id},{row_type},{row_title},{row_tags},{row_duration}"); } } } else if !extended { @@ -560,7 +559,6 @@ pub async fn run_cmd_get_album( "Id".to_string(), "Type".to_string(), "Title".to_string(), - "Description".to_string(), "Tags".to_string(), "Duration".to_string(), ]; @@ -572,7 +570,6 @@ pub async fn run_cmd_get_album( identifier_to_string(item.id).clone(), item.media_type.to_type_string(), to_csv_string(&item.title), - to_csv_string(&item.description), to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")), render_media_duration(item.media_type, item.duration.unwrap_or(0.0)), ]); diff --git a/src/commands/batch_operation.rs b/src/commands/batch_operation.rs index a968a82..2049c8c 100644 --- a/src/commands/batch_operation.rs +++ b/src/commands/batch_operation.rs @@ -6,7 +6,9 @@ use clap::Subcommand; use crate::{ api::{ - api_call_album_add_media, api_call_album_remove_media, api_call_get_album, api_call_get_tags, api_call_media_delete, api_call_search_advanced, api_call_tag_add, api_call_tag_remove, MAX_API_TAGS_FILTER, MAX_SEARCH_PAGE_LIMIT + api_call_album_add_media, api_call_album_remove_media, api_call_get_album, + api_call_get_tags, api_call_media_delete, api_call_search_advanced, api_call_tag_add, + api_call_tag_remove, MAX_API_TAGS_FILTER, MAX_SEARCH_PAGE_LIMIT, }, models::{ parse_media_type, parse_tag_name, parse_tag_search_mode, tags_map_from_list, @@ -58,7 +60,6 @@ pub enum BatchCommand { pub async fn run_cmd_batch_operation( global_opts: CommandGlobalOptions, title: Option<String>, - description: Option<String>, media_type: Option<String>, tags: Option<String>, tags_mode: Option<String>, @@ -126,8 +127,7 @@ pub async fn run_cmd_batch_operation( match album_id_res { Ok(_) => { let album_get_api_res = - api_call_get_album(&vault_url, album_id_res.unwrap(), global_opts.debug) - .await; + api_call_get_album(&vault_url, album_id_res.unwrap(), global_opts.debug).await; match album_get_api_res { Ok(album_data) => { @@ -159,7 +159,6 @@ pub async fn run_cmd_batch_operation( // Params let title_filter = title.unwrap_or("".to_string()); - let description_filter = description.unwrap_or("".to_string()); let mut media_type_filter: Option<MediaType> = None; @@ -233,7 +232,7 @@ pub async fn run_cmd_batch_operation( let mut tags_filter_mode = TagSearchMode::All; - if let Some(tags_mode_str) = tags_mode{ + if let Some(tags_mode_str) = tags_mode { let tags_mode_res = parse_tag_search_mode(&tags_mode_str); match tags_mode_res { @@ -265,31 +264,26 @@ pub async fn run_cmd_batch_operation( } else { "anyof".to_string() } - }, - TagSearchMode::None => { - "noneof".to_string() - }, + } + TagSearchMode::None => "noneof".to_string(), TagSearchMode::Untagged => "allof".to_string(), }; match tags_filter_mode { - TagSearchMode::All => {}, + TagSearchMode::All => {} TagSearchMode::Any => { if tags_filter_count > MAX_API_TAGS_FILTER { tag_param = None } - }, - TagSearchMode::None => {}, - TagSearchMode::Untagged => { - tag_param = None - }, + } + TagSearchMode::None => {} + TagSearchMode::Untagged => tag_param = None, } if everything { if tags_filter_mode != TagSearchMode::All || tag_param.is_some() || media_type_filter.is_some() - || !description_filter.is_empty() || !title_filter.is_empty() || album_filter.is_some() { @@ -309,7 +303,6 @@ pub async fn run_cmd_batch_operation( } else if tags_filter_mode == TagSearchMode::All && tag_param.is_none() && media_type_filter.is_none() - && description_filter.is_empty() && title_filter.is_empty() && album_filter.is_none() { @@ -337,7 +330,6 @@ pub async fn run_cmd_batch_operation( if media_matches_filter( &item, &title_filter, - &description_filter, &media_type_filter, &tags_filter, &tags_filter_mode, @@ -368,7 +360,6 @@ pub async fn run_cmd_batch_operation( if media_matches_filter( &item, &title_filter, - &description_filter, &media_type_filter, &tags_filter, &tags_filter_mode, @@ -379,7 +370,7 @@ pub async fn run_cmd_batch_operation( if search_result.scanned >= search_result.total_count { advanced_search_finished = true; - } + } continue_ref = Some(search_result.continue_ref); } diff --git a/src/commands/home.rs b/src/commands/home.rs new file mode 100644 index 0000000..4433dc3 --- /dev/null +++ b/src/commands/home.rs @@ -0,0 +1,664 @@ +// Home command + +use std::process; + +use clap::Subcommand; + +use crate::{ + api::{ + api_call_delete_home_group, api_call_get_home_group_elements, api_call_get_home_groups, + api_call_home_add_group, api_call_move_home_group, api_call_rename_home_group, + api_call_set_home_group_elements, + }, + commands::logout::do_logout, + models::{ + HomePageAddGroupBody, HomePageElementRef, HomePageGroupMoveBody, HomePageGroupRenameBody, + HomePageGroupSetElementsBody, HomePageGroupType, + }, + tools::{ensure_login, parse_vault_uri, print_table}, +}; + +use super::{get_vault_url, print_request_error, CommandGlobalOptions}; + +#[derive(Subcommand)] +pub enum HomeCommand { + /// Gets the groups in the home page + GetGroups, + + /// Adds a home page group + AddGroup { + /// A name for the group + name: String, + + /// The type of group (CUSTOM, RECENT_MEDIA or RECENT_ALBUMS) + group_type: String, + + /// Add this option in order to add the group at the top of the list + #[arg(long)] + prepend: bool, + }, + + /// Gets the elements of a group + GetGroupElements { + /// ID of the group + id: u64, + + /// Add this option to print the elements as references + #[arg(long)] + as_refs: bool, + }, + + /// Sets the elements of a group + SetGroupElements { + /// ID of the group + id: u64, + + /// List of group elements, as references, separated by commas. For media elements use M{ID} and for albums use A{ID}. Example: A12, M6, M8 + elements: String, + }, + + /// Renames an existing group + RenameGroup { + /// ID of the group + id: u64, + + /// A name for the group + name: String, + }, + + /// Moves an existing group to another position + MoveGroup { + /// ID of the group + id: u64, + + /// The position to move the group + position: u32, + }, + + /// Deletes an existing group + DeleteGroup { + /// ID of the group + id: u64, + }, +} + +pub async fn run_home_cmd(global_opts: CommandGlobalOptions, cmd: HomeCommand) { + match cmd { + HomeCommand::GetGroups => { + run_cmd_home_get_groups(global_opts).await; + } + HomeCommand::AddGroup { + name, + group_type, + prepend, + } => { + run_cmd_home_add_group(global_opts, name, group_type, prepend).await; + } + HomeCommand::GetGroupElements { id, as_refs } => { + run_cmd_home_get_group_elements(global_opts, id, as_refs).await; + } + HomeCommand::SetGroupElements { id, elements } => { + run_cmd_home_set_group_elements(global_opts, id, elements).await; + } + HomeCommand::RenameGroup { id, name } => { + run_cmd_home_rename_group(global_opts, id, name).await; + } + HomeCommand::MoveGroup { id, position } => { + run_cmd_home_move_group(global_opts, id, position).await; + } + HomeCommand::DeleteGroup { id } => { + run_cmd_home_delete_group(global_opts, id).await; + } + } +} + +pub async fn run_cmd_home_get_groups(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_get_home_groups(&vault_url, global_opts.debug).await; + + match api_res { + Ok(groups) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + let table_head: Vec<String> = vec![ + "Id".to_string(), + "Type".to_string(), + "Name".to_string(), + "Custom elements".to_string(), + ]; + let mut table_body: Vec<Vec<String>> = Vec::with_capacity(groups.len()); + + for group in groups { + table_body.push(vec![ + group.id.to_string(), + group.group_type.as_string(), + group.name.clone().unwrap_or("".to_string()), + group.elements_count.unwrap_or(0).to_string(), + ]); + } + + print_table(&table_head, &table_body, false); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_home_add_group( + global_opts: CommandGlobalOptions, + name: String, + group_type: String, + prepend: bool, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let group_type_model = match HomePageGroupType::from_string(&group_type) { + Some(t) => t, + None => { + eprintln!( + "Invalid group type: {}. Valid ones are: CUSTOM, RECENT_MEDIA and RECENT_ALBUMS", + group_type + ); + process::exit(1); + } + }; + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_home_add_group( + &vault_url, + HomePageAddGroupBody { + name: Some(name.clone()), + group_type: group_type_model, + prepend, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(res) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("Successfully added group {} to home page", res.id); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_home_get_group_elements( + global_opts: CommandGlobalOptions, + id: u64, + as_refs: bool, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_get_home_group_elements(&vault_url, id, global_opts.debug).await; + + match api_res { + Ok(elements) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + if as_refs { + let element_refs = HomePageElementRef::from_home_page_elements(&elements); + let list_string = HomePageElementRef::as_list_string(&element_refs); + + println!("{}", list_string); + } else { + let table_head: Vec<String> = + vec!["Id".to_string(), "Type".to_string(), "Title".to_string()]; + let mut table_body: Vec<Vec<String>> = Vec::with_capacity(elements.len()); + + for element in elements { + if !element.is_valid_element() { + continue; + } + + table_body.push(vec![ + element.get_element_id().to_string(), + element.get_element_type().as_string(), + element.get_element_title(), + ]); + } + + print_table(&table_head, &table_body, false); + } + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_home_set_group_elements( + global_opts: CommandGlobalOptions, + id: u64, + elements: String, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let elements_refs = match HomePageElementRef::from_list_string(&elements) { + Ok(r) => r, + Err(pos) => { + eprintln!("Invalid element reference as position {}", pos); + process::exit(1); + } + }; + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_set_home_group_elements( + &vault_url, + id, + HomePageGroupSetElementsBody { + elements: elements_refs, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("Successfully set elements for group {}", id); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_home_rename_group(global_opts: CommandGlobalOptions, id: u64, name: String) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_rename_home_group( + &vault_url, + id, + HomePageGroupRenameBody { + name: Some(name.clone()), + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("Successfully renamed group {} to '{}'", id, name); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_home_move_group(global_opts: CommandGlobalOptions, id: u64, position: u32) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_move_home_group( + &vault_url, + id, + HomePageGroupMoveBody { position }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("Successfully moved group {} to position {}", id, position); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_home_delete_group(global_opts: CommandGlobalOptions, id: u64) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_delete_home_group(&vault_url, id, global_opts.debug).await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("Successfully deleted group {} from the home page", id); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} diff --git a/src/commands/media.rs b/src/commands/media.rs index 5f464ff..cf9e33f 100644 --- a/src/commands/media.rs +++ b/src/commands/media.rs @@ -7,17 +7,18 @@ use clap::Subcommand; use crate::{ api::{ api_call_get_media, api_call_get_media_stats, api_call_get_tags, - api_call_media_change_description, api_call_media_change_extra, - api_call_media_change_title, api_call_media_delete, api_call_media_re_encode, + api_call_media_change_extra, api_call_media_change_related, api_call_media_change_title, + api_call_media_delete, api_call_media_re_encode, }, commands::logout::do_logout, models::{ - tags_map_from_list, tags_names_from_ids, MediaUpdateDescriptionBody, MediaUpdateExtraBody, - MediaUpdateTitleBody, + tags_map_from_list, tags_names_from_ids, MediaListItem, MediaUpdateExtraBody, + MediaUpdateRelatedMediaBody, MediaUpdateTitleBody, }, tools::{ ask_user, duration_to_string, ensure_login, format_date, identifier_to_string, - parse_identifier, parse_vault_uri, render_size_bytes, to_csv_string, + parse_identifier, parse_vault_uri, print_table, render_media_duration, render_size_bytes, + to_csv_string, }, }; @@ -31,9 +32,9 @@ use super::{ run_cmd_delete_media_audio_track, run_cmd_rename_media_audio_track, run_cmd_upload_media_audio_track, }, + media_description::run_cmd_set_media_extended_description, media_download::run_cmd_download_media, media_export::run_cmd_export_media, - media_extended_description::run_cmd_set_media_extended_description, media_image_notes::run_cmd_set_media_image_notes, media_import::run_cmd_import_media, media_replace::run_cmd_replace_media, @@ -78,6 +79,20 @@ pub enum MediaCommand { print_link: bool, }, + /// Gets related media + GetRelated { + /// Media asset ID + media: String, + + /// Extended version of the results table + #[arg(short, long)] + extended: bool, + + /// CSV format + #[arg(short, long)] + csv: bool, + }, + /// Exports a media asset, downloading everything (metadata + assets) into a folder Export { /// Media asset ID @@ -134,17 +149,17 @@ pub enum MediaCommand { /// Media asset ID media: String, - /// Description - description: String, + /// Path to the text file containing the description + path: String, }, - /// Changes the extended description of a media asset - SetExtendedDescription { + /// Sets the related media list + SetRelatedMedia { /// Media asset ID media: String, - /// Path to the text file containing the extended description - path: String, + /// List of related media IDs, separated by commas + related: String, }, /// Changes the forced start from beginning parameter of a media asset @@ -380,9 +395,6 @@ pub async fn run_media_cmd(global_opts: CommandGlobalOptions, cmd: MediaCommand) MediaCommand::SetTitle { media, title } => { run_cmd_media_set_title(global_opts, media, title).await; } - MediaCommand::SetDescription { media, description } => { - run_cmd_media_set_description(global_opts, media, description).await; - } MediaCommand::SetForceStartBeginning { media, force_start_beginning, @@ -445,7 +457,7 @@ pub async fn run_media_cmd(global_opts: CommandGlobalOptions, cmd: MediaCommand) MediaCommand::Delete { media } => { run_cmd_media_delete(global_opts, media).await; } - MediaCommand::SetExtendedDescription { media, path } => { + MediaCommand::SetDescription { media, path } => { run_cmd_set_media_extended_description(global_opts, media, path).await; } MediaCommand::Export { media, output } => { @@ -486,6 +498,16 @@ pub async fn run_media_cmd(global_opts: CommandGlobalOptions, cmd: MediaCommand) } => { run_cmd_rename_media_audio_track(global_opts, media, track_id, new_id, new_name).await; } + MediaCommand::GetRelated { + media, + extended, + csv, + } => { + run_cmd_get_media_related(global_opts, media, csv, extended).await; + } + MediaCommand::SetRelatedMedia { media, related } => { + run_cmd_media_set_related(global_opts, media, related).await; + } } } @@ -629,9 +651,6 @@ pub async fn run_cmd_get_media(global_opts: CommandGlobalOptions, media: String) let out_title = to_csv_string(&media_data.title); println!("Title: {out_title}"); - let out_description = to_csv_string(&media_data.description); - println!("Description: {out_description}"); - if !media_data.thumbnail.is_empty() { let out_thumbnail = media_data.thumbnail; println!("Thumbnail: {out_thumbnail}"); @@ -754,8 +773,8 @@ pub async fn run_cmd_get_media(global_opts: CommandGlobalOptions, media: String) } } - if let Some(ext_desc_url) = media_data.ext_desc_url { - println!("Extended description: {ext_desc_url}"); + if let Some(ext_desc_url) = media_data.description_url { + println!("Description: {ext_desc_url}"); } if let Some(time_slices) = media_data.time_slices { @@ -1017,10 +1036,10 @@ pub async fn run_cmd_media_set_title( } } -pub async fn run_cmd_media_set_description( +pub async fn run_cmd_media_set_force_start_beginning( global_opts: CommandGlobalOptions, media: String, - description: String, + force_start_beginning: String, ) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); @@ -1096,13 +1115,44 @@ pub async fn run_cmd_media_set_description( } } + // Param + + let force_start_beginning_lower = force_start_beginning.to_lowercase(); + let force_start_beginning_bool: bool; + + if force_start_beginning_lower == "true" + || force_start_beginning_lower == "yes" + || force_start_beginning_lower == "1" + { + force_start_beginning_bool = true; + } else if force_start_beginning_lower == "false" + || force_start_beginning_lower == "no" + || force_start_beginning_lower == "0" + { + force_start_beginning_bool = false; + } else { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + eprintln!("Invalid FORCE_START_BEGINNING parameter. Set it to 'true' or 'false'."); + process::exit(1); + } + // Call API - let api_res = api_call_media_change_description( + let api_res = api_call_media_change_extra( &vault_url, media_id_param, - MediaUpdateDescriptionBody { - description: description.clone(), + MediaUpdateExtraBody { + force_start_beginning: Some(force_start_beginning_bool), + is_anim: None, }, global_opts.debug, ) @@ -1121,11 +1171,7 @@ pub async fn run_cmd_media_set_description( } } - let description_csv = to_csv_string(&description); - - eprintln!( - "Successfully updated the description of #{media_id_param}: {description_csv}" - ); + eprintln!("Successfully updated the force-start-beginning param of #{media_id_param}: {force_start_beginning_bool}"); } Err(e) => { print_request_error(e); @@ -1144,10 +1190,10 @@ pub async fn run_cmd_media_set_description( } } -pub async fn run_cmd_media_set_force_start_beginning( +pub async fn run_cmd_media_set_is_anim( global_opts: CommandGlobalOptions, media: String, - force_start_beginning: String, + is_anim: String, ) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); @@ -1225,19 +1271,13 @@ pub async fn run_cmd_media_set_force_start_beginning( // Param - let force_start_beginning_lower = force_start_beginning.to_lowercase(); - let force_start_beginning_bool: bool; + let is_anim_lower = is_anim.to_lowercase(); + let is_anim_bool: bool; - if force_start_beginning_lower == "true" - || force_start_beginning_lower == "yes" - || force_start_beginning_lower == "1" - { - force_start_beginning_bool = true; - } else if force_start_beginning_lower == "false" - || force_start_beginning_lower == "no" - || force_start_beginning_lower == "0" - { - force_start_beginning_bool = false; + if is_anim_lower == "true" || is_anim_lower == "yes" || is_anim_lower == "1" { + is_anim_bool = true; + } else if is_anim_lower == "false" || is_anim_lower == "no" || is_anim_lower == "0" { + is_anim_bool = false; } else { if logout_after_operation { let logout_res = do_logout(&global_opts, &vault_url).await; @@ -1249,7 +1289,7 @@ pub async fn run_cmd_media_set_force_start_beginning( } } } - eprintln!("Invalid FORCE_START_BEGINNING parameter. Set it to 'true' or 'false'."); + eprintln!("Invalid IS_ANIMATION parameter. Set it to 'true' or 'false'."); process::exit(1); } @@ -1259,8 +1299,8 @@ pub async fn run_cmd_media_set_force_start_beginning( &vault_url, media_id_param, MediaUpdateExtraBody { - force_start_beginning: Some(force_start_beginning_bool), - is_anim: None, + force_start_beginning: None, + is_anim: Some(is_anim_bool), }, global_opts.debug, ) @@ -1279,7 +1319,9 @@ pub async fn run_cmd_media_set_force_start_beginning( } } - eprintln!("Successfully updated the force-start-beginning param of #{media_id_param}: {force_start_beginning_bool}"); + eprintln!( + "Successfully updated the is-animation param of #{media_id_param}: {is_anim_bool}" + ); } Err(e) => { print_request_error(e); @@ -1298,11 +1340,7 @@ pub async fn run_cmd_media_set_force_start_beginning( } } -pub async fn run_cmd_media_set_is_anim( - global_opts: CommandGlobalOptions, - media: String, - is_anim: String, -) { +pub async fn run_cmd_media_re_encode(global_opts: CommandGlobalOptions, media: String) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); if url_parse_res.is_err() { @@ -1377,42 +1415,30 @@ pub async fn run_cmd_media_set_is_anim( } } - // Param + // Ask confirmation - let is_anim_lower = is_anim.to_lowercase(); - let is_anim_bool: bool; + if !global_opts.auto_confirm { + eprintln!("Are you sure you want to re-encode the media asset {media_id_param}?"); + let confirmation = ask_user("Continue? y/n: ").await.unwrap_or("".to_string()); - if is_anim_lower == "true" || is_anim_lower == "yes" || is_anim_lower == "1" { - is_anim_bool = true; - } else if is_anim_lower == "false" || is_anim_lower == "no" || is_anim_lower == "0" { - is_anim_bool = false; - } else { - if logout_after_operation { - let logout_res = do_logout(&global_opts, &vault_url).await; + if confirmation.to_lowercase() != "y" { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; - match logout_res { - Ok(_) => {} - Err(_) => { - process::exit(1); + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } } } + process::exit(1); } - eprintln!("Invalid IS_ANIMATION parameter. Set it to 'true' or 'false'."); - process::exit(1); } // Call API - let api_res = api_call_media_change_extra( - &vault_url, - media_id_param, - MediaUpdateExtraBody { - force_start_beginning: None, - is_anim: Some(is_anim_bool), - }, - global_opts.debug, - ) - .await; + let api_res = api_call_media_re_encode(&vault_url, media_id_param, global_opts.debug).await; match api_res { Ok(_) => { @@ -1427,9 +1453,7 @@ pub async fn run_cmd_media_set_is_anim( } } - eprintln!( - "Successfully updated the is-animation param of #{media_id_param}: {is_anim_bool}" - ); + eprintln!("Successfully requested media asset #{media_id_param} to be re-encoded"); } Err(e) => { print_request_error(e); @@ -1448,7 +1472,7 @@ pub async fn run_cmd_media_set_is_anim( } } -pub async fn run_cmd_media_re_encode(global_opts: CommandGlobalOptions, media: String) { +pub async fn run_cmd_media_delete(global_opts: CommandGlobalOptions, media: String) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); if url_parse_res.is_err() { @@ -1526,7 +1550,7 @@ pub async fn run_cmd_media_re_encode(global_opts: CommandGlobalOptions, media: S // Ask confirmation if !global_opts.auto_confirm { - eprintln!("Are you sure you want to re-encode the media asset {media_id_param}?"); + eprintln!("Are you sure you want to delete the media asset {media_id_param}?"); let confirmation = ask_user("Continue? y/n: ").await.unwrap_or("".to_string()); if confirmation.to_lowercase() != "y" { @@ -1546,7 +1570,7 @@ pub async fn run_cmd_media_re_encode(global_opts: CommandGlobalOptions, media: S // Call API - let api_res = api_call_media_re_encode(&vault_url, media_id_param, global_opts.debug).await; + let api_res = api_call_media_delete(&vault_url, media_id_param, global_opts.debug).await; match api_res { Ok(_) => { @@ -1561,7 +1585,7 @@ pub async fn run_cmd_media_re_encode(global_opts: CommandGlobalOptions, media: S } } - eprintln!("Successfully requested media asset #{media_id_param} to be re-encoded"); + eprintln!("Successfully deleted asset #{media_id_param}"); } Err(e) => { print_request_error(e); @@ -1580,7 +1604,188 @@ pub async fn run_cmd_media_re_encode(global_opts: CommandGlobalOptions, media: S } } -pub async fn run_cmd_media_delete(global_opts: CommandGlobalOptions, media: String) { +pub async fn run_cmd_get_media_related( + global_opts: CommandGlobalOptions, + media: String, + csv: bool, + extended: bool, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Params + + let media_id_res = parse_identifier(&media); + let media_id: u64 = match media_id_res { + Ok(id) => id, + Err(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + eprintln!("Invalid media identifier specified."); + process::exit(1); + } + }; + + // Get tags + + let tags_res = api_call_get_tags(&vault_url, global_opts.debug).await; + + if tags_res.is_err() { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + print_request_error(tags_res.err().unwrap()); + process::exit(1); + } + + let tags = tags_map_from_list(&tags_res.unwrap()); + + // Call API + + let api_res = api_call_get_media(&vault_url, media_id, global_opts.debug).await; + + match api_res { + Ok(media_data) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + let related_media: Vec<MediaListItem> = match media_data.related { + Some(r) => r.clone(), + None => Vec::new(), + }; + + if csv { + println!(); + if !extended { + println!("\"Id\",\"Type\",\"Title\""); + + for item in related_media.iter() { + let row_id = item.id.to_string(); + let row_type = to_csv_string(&item.media_type.to_type_string()); + let row_title = to_csv_string(&item.title); + println!("{row_id},{row_type},{row_title}"); + } + } else { + println!("\"Id\",\"Type\",\"Title\",\"Tags\",\"Duration\""); + + for item in related_media.iter() { + let row_id = item.id.to_string(); + let row_type = to_csv_string(&item.media_type.to_type_string()); + let row_title = to_csv_string(&item.title); + let row_tags = + to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")); + let row_duration = + render_media_duration(item.media_type, item.duration.unwrap_or(0.0)); + + println!("{row_id},{row_type},{row_title},{row_tags},{row_duration}"); + } + } + } else if !extended { + let table_head: Vec<String> = + vec!["Id".to_string(), "Type".to_string(), "Title".to_string()]; + let mut table_body: Vec<Vec<String>> = Vec::with_capacity(related_media.len()); + + for item in related_media.iter() { + table_body.push(vec![ + identifier_to_string(item.id).clone(), + item.media_type.to_type_string(), + to_csv_string(&item.title), + ]); + } + + print_table(&table_head, &table_body, false); + } else { + let table_head: Vec<String> = vec![ + "Id".to_string(), + "Type".to_string(), + "Title".to_string(), + "Tags".to_string(), + "Duration".to_string(), + ]; + let mut table_body: Vec<Vec<String>> = Vec::with_capacity(related_media.len()); + + for item in related_media.iter() { + table_body.push(vec![ + identifier_to_string(item.id).clone(), + item.media_type.to_type_string(), + to_csv_string(&item.title), + to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")), + render_media_duration(item.media_type, item.duration.unwrap_or(0.0)), + ]); + } + + print_table(&table_head, &table_body, false); + } + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_media_set_related( + global_opts: CommandGlobalOptions, + media: String, + related: String, +) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); if url_parse_res.is_err() { @@ -1655,30 +1860,40 @@ pub async fn run_cmd_media_delete(global_opts: CommandGlobalOptions, media: Stri } } - // Ask confirmation - - if !global_opts.auto_confirm { - eprintln!("Are you sure you want to delete the media asset {media_id_param}?"); - let confirmation = ask_user("Continue? y/n: ").await.unwrap_or("".to_string()); + let related_str: Vec<&str> = related.split(",").filter(|s| !s.is_empty()).collect(); + let mut related: Vec<u64> = Vec::with_capacity(related_str.len()); - if confirmation.to_lowercase() != "y" { - if logout_after_operation { - let logout_res = do_logout(&global_opts, &vault_url).await; + for s in related_str { + match parse_identifier(s) { + Ok(i) => { + related.push(i); + } + Err(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; - match logout_res { - Ok(_) => {} - Err(_) => { - process::exit(1); + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } } } + eprintln!("Invalid media asset identifier specified: {}", s); + process::exit(1); } - process::exit(1); } } // Call API - let api_res = api_call_media_delete(&vault_url, media_id_param, global_opts.debug).await; + let api_res = api_call_media_change_related( + &vault_url, + media_id_param, + MediaUpdateRelatedMediaBody { related }, + global_opts.debug, + ) + .await; match api_res { Ok(_) => { @@ -1693,7 +1908,7 @@ pub async fn run_cmd_media_delete(global_opts: CommandGlobalOptions, media: Stri } } - eprintln!("Successfully deleted asset #{media_id_param}"); + eprintln!("Successfully updated the related media of #{media_id_param}"); } Err(e) => { print_request_error(e); diff --git a/src/commands/media_extended_description.rs b/src/commands/media_description.rs similarity index 97% rename from src/commands/media_extended_description.rs rename to src/commands/media_description.rs index 5b88df9..afdab25 100644 --- a/src/commands/media_extended_description.rs +++ b/src/commands/media_description.rs @@ -5,7 +5,7 @@ use std::process; use crate::{ api::{api_call_get_media, api_call_media_change_extended_description}, commands::logout::do_logout, - models::MediaUpdateExtendedDescriptionBody, + models::MediaUpdateDescriptionBody, tools::{ensure_login, parse_identifier, parse_vault_uri}, }; @@ -107,7 +107,7 @@ pub async fn run_cmd_set_media_extended_description( let api_res = api_call_media_change_extended_description( &vault_url, media_id_param, - MediaUpdateExtendedDescriptionBody { ext_desc }, + MediaUpdateDescriptionBody { description: ext_desc }, global_opts.debug, ) .await; diff --git a/src/commands/media_download.rs b/src/commands/media_download.rs index 9f062c8..efa348f 100644 --- a/src/commands/media_download.rs +++ b/src/commands/media_download.rs @@ -25,7 +25,7 @@ pub enum DownloadAssetType { Audio(String), VideoPreview(u32), Notes, - ExtendedDescription, + Description, Attachment(u64), } @@ -45,8 +45,8 @@ pub fn parse_asset_type(s: &str) -> Result<DownloadAssetType, ()> { Ok(DownloadAssetType::Thumbnail) } else if parts_type == "notes" { Ok(DownloadAssetType::Notes) - } else if parts_type == "ext_desc" { - Ok(DownloadAssetType::ExtendedDescription) + } else if parts_type == "desc" || parts_type == "description" || parts_type == "ext_desc" { + Ok(DownloadAssetType::Description) } else if parts_type == "resolution" || parts_type == "res" || parts_type == "r" { // Try video resolution let video_res = ConfigVideoResolution::from_str(&val); @@ -582,7 +582,7 @@ pub async fn run_cmd_download_media( process::exit(1); } }, - DownloadAssetType::ExtendedDescription => match media_data.ext_desc_url { + DownloadAssetType::Description => match media_data.description_url { Some(u) => { if u.is_empty() { if logout_after_operation { diff --git a/src/commands/media_export.rs b/src/commands/media_export.rs index 4bc99a8..1ac5181 100644 --- a/src/commands/media_export.rs +++ b/src/commands/media_export.rs @@ -181,7 +181,6 @@ pub async fn run_cmd_export_media( notes: None, ext_desc: None, title: None, - description: None, tags: None, force_start_beginning: None, is_anim: None, @@ -192,7 +191,6 @@ pub async fn run_cmd_export_media( }; out_metadata.title = Some(media_metadata.title.clone()); - out_metadata.description = Some(media_metadata.description.clone()); out_metadata.force_start_beginning = media_metadata.force_start_beginning; out_metadata.is_anim = media_metadata.is_anim; out_metadata.time_slices = media_metadata.time_slices.clone(); @@ -306,7 +304,7 @@ pub async fn run_cmd_export_media( // Extended description - if let Some(ext_desc_url) = media_metadata.ext_desc_url { + if let Some(ext_desc_url) = media_metadata.description_url { if !ext_desc_url.is_empty() { let ext = get_extension_from_url(&ext_desc_url, "txt"); let out_file_name = "ext_desc".to_owned() + "." + &ext; @@ -320,7 +318,7 @@ pub async fn run_cmd_export_media( download_media_asset( global_opts.clone(), &vault_url, - "extended description", + "description", ext_desc_url, ext_desc_out_path, logout_after_operation, diff --git a/src/commands/media_import.rs b/src/commands/media_import.rs index 659c8b5..b05d4ac 100644 --- a/src/commands/media_import.rs +++ b/src/commands/media_import.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ api::{ - api_call_get_media, api_call_media_add_attachment, api_call_media_change_description, + api_call_get_media, api_call_media_add_attachment, api_call_media_change_extended_description, api_call_media_change_extra, api_call_media_change_notes, api_call_media_change_thumbnail, api_call_media_change_time_slices, api_call_media_rename_attachment, @@ -17,11 +17,10 @@ use crate::{ commands::logout::do_logout, models::{ AddTagBody, ImageNote, MediaMetadataExport, MediaRenameAttachmentBody, - MediaUpdateDescriptionBody, MediaUpdateExtendedDescriptionBody, MediaUpdateExtraBody, + MediaUpdateDescriptionBody, MediaUpdateExtraBody, }, tools::{ - ensure_login, identifier_to_string, parse_identifier, parse_vault_uri, to_csv_string, - ProgressReceiver, + ensure_login, identifier_to_string, parse_identifier, parse_vault_uri, ProgressReceiver, }, }; @@ -285,35 +284,6 @@ pub async fn run_cmd_import_media( } } - // Set description - - if let Some(description) = import_metadata.description { - if !description.is_empty() { - let api_res = api_call_media_change_description( - &vault_url, - media_id, - MediaUpdateDescriptionBody { - description: description.clone(), - }, - global_opts.debug, - ) - .await; - - match api_res { - Ok(_) => { - let description_csv = to_csv_string(&description); - - eprintln!( - "Successfully updated the description of {media_id_str}: {description_csv}" - ); - } - Err(e) => { - print_request_error(e); - } - } - } - } - // Set extra configuration let api_res = api_call_media_change_extra( @@ -386,7 +356,9 @@ pub async fn run_cmd_import_media( let api_res = api_call_media_change_extended_description( &vault_url, media_id, - MediaUpdateExtendedDescriptionBody { ext_desc }, + MediaUpdateDescriptionBody { + description: ext_desc, + }, global_opts.debug, ) .await; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 39e3873..011ddb6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -27,6 +27,9 @@ pub use disk_usage::*; mod invites; use invites::*; +mod home; +use home::*; + mod login; use login::*; @@ -40,7 +43,7 @@ mod media_attachments; mod media_audio_tracks; mod media_download; mod media_export; -mod media_extended_description; +mod media_description; mod media_image_notes; mod media_import; mod media_replace; @@ -172,10 +175,6 @@ pub enum Commands { #[arg(short = 'q', long)] title: Option<String>, - /// Filter by description. - #[arg(short, long)] - description: Option<String>, - /// Filter by media type. Can be: video, audio or image #[arg(short = 'k', long)] media_type: Option<String>, @@ -247,16 +246,18 @@ pub enum Commands { invites_cmd: InvitesCommand, }, + /// Manges the home page + Home { + #[command(subcommand)] + home_cmd: HomeCommand, + }, + /// Applies a batch operation to a list of media assets Batch { /// Filter by title #[arg(short = 'q', long)] title: Option<String>, - /// Filter by description. - #[arg(short, long)] - description: Option<String>, - /// Filter by media type. Can be: video, audio or image #[arg(short = 'k', long)] media_type: Option<String>, @@ -330,7 +331,6 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { } Commands::AdvancedSearch { title, - description, media_type, tags, tags_mode, @@ -344,7 +344,6 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { run_cmd_search_advanced( global_opts, title, - description, media_type, tags, tags_mode, @@ -371,7 +370,6 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { } Commands::Batch { title, - description, media_type, tags, tags_mode, @@ -382,7 +380,6 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { run_cmd_batch_operation( global_opts, title, - description, media_type, tags, tags_mode, @@ -401,6 +398,9 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { Commands::GetDiskUsage => { run_cmd_disk_usage(global_opts).await; } + Commands::Home { home_cmd } => { + run_home_cmd(global_opts, home_cmd).await; + } } } diff --git a/src/commands/random.rs b/src/commands/random.rs index 914d564..c98dc62 100644 --- a/src/commands/random.rs +++ b/src/commands/random.rs @@ -152,19 +152,18 @@ pub async fn run_cmd_random( println!("{row_id},{row_type},{row_title}"); } } else { - println!("\"Id\",\"Type\",\"Title\",\"Description\",\"Tags\",\"Duration\""); + println!("\"Id\",\"Type\",\"Title\",\"Tags\",\"Duration\""); for item in random_result.page_items { let row_id = item.id.to_string(); let row_type = to_csv_string(&item.media_type.to_type_string()); let row_title = to_csv_string(&item.title); - let row_description = to_csv_string(&item.description); let row_tags = to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")); let row_duration = render_media_duration(item.media_type, item.duration.unwrap_or(0.0)); - println!("{row_id},{row_type},{row_title},{row_description},{row_tags},{row_duration}"); + println!("{row_id},{row_type},{row_title},{row_tags},{row_duration}"); } } } else if !extended { @@ -186,7 +185,6 @@ pub async fn run_cmd_random( "Id".to_string(), "Type".to_string(), "Title".to_string(), - "Description".to_string(), "Tags".to_string(), "Duration".to_string(), ]; @@ -197,7 +195,6 @@ pub async fn run_cmd_random( identifier_to_string(item.id).clone(), item.media_type.to_type_string(), to_csv_string(&item.title), - to_csv_string(&item.description), to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")), render_media_duration(item.media_type, item.duration.unwrap_or(0.0)), ]); diff --git a/src/commands/search_advanced.rs b/src/commands/search_advanced.rs index 5b49bbe..8cb86cb 100644 --- a/src/commands/search_advanced.rs +++ b/src/commands/search_advanced.rs @@ -22,7 +22,6 @@ const DEFAULT_RESULTS_LIMIT: u32 = 25; pub async fn run_cmd_search_advanced( global_opts: CommandGlobalOptions, title: Option<String>, - description: Option<String>, media_type: Option<String>, tags: Option<String>, tags_mode: Option<String>, @@ -154,7 +153,6 @@ pub async fn run_cmd_search_advanced( let limit_param = limit.unwrap_or(DEFAULT_RESULTS_LIMIT); let title_filter = title.unwrap_or("".to_string()); - let description_filter = description.unwrap_or("".to_string()); let mut media_type_filter: Option<MediaType> = None; @@ -288,7 +286,6 @@ pub async fn run_cmd_search_advanced( if media_matches_filter( item, &title_filter, - &description_filter, &media_type_filter, &tags_filter, &tags_filter_mode, @@ -305,7 +302,6 @@ pub async fn run_cmd_search_advanced( if media_matches_filter( &item, &title_filter, - &description_filter, &media_type_filter, &tags_filter, &tags_filter_mode, @@ -346,7 +342,6 @@ pub async fn run_cmd_search_advanced( if media_matches_filter( &item, &title_filter, - &description_filter, &media_type_filter, &tags_filter, &tags_filter_mode, @@ -414,19 +409,18 @@ pub async fn run_cmd_search_advanced( println!("{row_id},{row_type},{row_title}"); } } else { - println!("\"Id\",\"Type\",\"Title\",\"Description\",\"Tags\",\"Duration\""); + println!("\"Id\",\"Type\",\"Title\",\"Tags\",\"Duration\""); for item in advanced_search_results { let row_id = item.id.to_string(); let row_type = to_csv_string(&item.media_type.to_type_string()); let row_title = to_csv_string(&item.title); - let row_description = to_csv_string(&item.description); let row_tags = to_csv_string(&tags_names_from_ids(&item.tags, &tags_map).join(" ")); let row_duration = render_media_duration(item.media_type, item.duration.unwrap_or(0.0)); println!( - "{row_id},{row_type},{row_title},{row_description},{row_tags},{row_duration}" + "{row_id},{row_type},{row_title},{row_tags},{row_duration}" ); } } @@ -449,7 +443,6 @@ pub async fn run_cmd_search_advanced( "Id".to_string(), "Type".to_string(), "Title".to_string(), - "Description".to_string(), "Tags".to_string(), "Duration".to_string(), ]; @@ -460,7 +453,6 @@ pub async fn run_cmd_search_advanced( identifier_to_string(item.id).clone(), item.media_type.to_type_string(), to_csv_string(&item.title), - to_csv_string(&item.description), to_csv_string(&tags_names_from_ids(&item.tags, &tags_map).join(" ")), render_media_duration(item.media_type, item.duration.unwrap_or(0.0)), ]); @@ -473,7 +465,6 @@ pub async fn run_cmd_search_advanced( pub fn media_matches_filter( media: &MediaListItem, title: &str, - description: &str, media_type_filter: &Option<MediaType>, tags_filter: &Option<Vec<u64>>, tags_filter_mode: &TagSearchMode, @@ -482,10 +473,6 @@ pub fn media_matches_filter( return false; } - if !description.is_empty() && !media.description.contains(description) { - return false; - } - if let Some(t) = media_type_filter { if *t != media.media_type { return false; diff --git a/src/commands/search_basic.rs b/src/commands/search_basic.rs index 58ea89c..8eb52d3 100644 --- a/src/commands/search_basic.rs +++ b/src/commands/search_basic.rs @@ -151,19 +151,18 @@ pub async fn run_cmd_search_basic( println!("{row_id},{row_type},{row_title}"); } } else { - println!("\"Id\",\"Type\",\"Title\",\"Description\",\"Tags\",\"Duration\""); + println!("\"Id\",\"Type\",\"Title\",\"Tags\",\"Duration\""); for item in search_result.page_items { let row_id = item.id.to_string(); let row_type = to_csv_string(&item.media_type.to_type_string()); let row_title = to_csv_string(&item.title); - let row_description = to_csv_string(&item.description); let row_tags = to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")); let row_duration = render_media_duration(item.media_type, item.duration.unwrap_or(0.0)); - println!("{row_id},{row_type},{row_title},{row_description},{row_tags},{row_duration}"); + println!("{row_id},{row_type},{row_title},{row_tags},{row_duration}"); } } } else if !extended { @@ -185,7 +184,6 @@ pub async fn run_cmd_search_basic( "Id".to_string(), "Type".to_string(), "Title".to_string(), - "Description".to_string(), "Tags".to_string(), "Duration".to_string(), ]; @@ -196,7 +194,6 @@ pub async fn run_cmd_search_basic( identifier_to_string(item.id).clone(), item.media_type.to_type_string(), to_csv_string(&item.title), - to_csv_string(&item.description), to_csv_string(&tags_names_from_ids(&item.tags, &tags).join(" ")), render_media_duration(item.media_type, item.duration.unwrap_or(0.0)), ]); diff --git a/src/models/home.rs b/src/models/home.rs new file mode 100644 index 0000000..8008e92 --- /dev/null +++ b/src/models/home.rs @@ -0,0 +1,242 @@ +// Models for home page API + +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::models::{AlbumListItem, MediaListItem}; + +#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)] +#[repr(u8)] +pub enum HomePageGroupType { + #[serde(other)] + Custom = 0, + RecentMedia = 1, + RecentAlbums = 2, +} + +impl HomePageGroupType { + pub fn as_string(&self) -> String { + match self { + HomePageGroupType::Custom => "CUSTOM".to_string(), + HomePageGroupType::RecentMedia => "RECENT_MEDIA".to_string(), + HomePageGroupType::RecentAlbums => "RECENT_ALBUMS".to_string(), + } + } + + pub fn from_string(s: &str) -> Option<Self> { + match s.to_uppercase().as_str() { + "CUSTOM" => Some(HomePageGroupType::Custom), + "RECENT_MEDIA" => Some(HomePageGroupType::RecentMedia), + "RECENT_ALBUMS" => Some(HomePageGroupType::RecentAlbums), + _ => None, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageGroup { + #[serde(rename = "id")] + pub id: u64, + + #[serde(rename = "type")] + pub group_type: HomePageGroupType, + + #[serde(rename = "name")] + pub name: Option<String>, + + #[serde(rename = "elementsCount")] + pub elements_count: Option<u32>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageAddGroupBody { + #[serde(rename = "name")] + pub name: Option<String>, + + #[serde(rename = "type")] + pub group_type: HomePageGroupType, + + #[serde(rename = "prepend")] + pub prepend: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageElement { + #[serde(rename = "media")] + media: Option<MediaListItem>, + + #[serde(rename = "album")] + album: Option<AlbumListItem>, +} + +impl HomePageElement { + pub fn is_valid_element(&self) -> bool { + self.album.is_some() || self.media.is_some() + } + + pub fn get_element_type(&self) -> HomePageElementType { + if self.album.is_some() { + HomePageElementType::Album + } else { + HomePageElementType::Media + } + } + + pub fn get_element_id(&self) -> u64 { + if let Some(a) = &self.album { + return a.id; + } + + if let Some(m) = &self.media { + return m.id; + } + + 0 + } + + pub fn get_element_title(&self) -> String { + if let Some(a) = &self.album { + return a.name.clone(); + } + + if let Some(m) = &self.media { + return m.title.clone(); + } + + "".to_string() + } +} + +#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)] +#[repr(u8)] +pub enum HomePageElementType { + #[serde(other)] + Media = 0, + Album = 1, +} + +impl HomePageElementType { + pub fn as_string(&self) -> String { + match self { + HomePageElementType::Media => "MEDIA".to_string(), + HomePageElementType::Album => "ALBUM".to_string(), + } + } + + pub fn as_prefix(&self) -> String { + match self { + HomePageElementType::Media => "M".to_string(), + HomePageElementType::Album => "A".to_string(), + } + } + + pub fn parse_prefix(prefix: &str) -> Option<HomePageElementType> { + match prefix.to_uppercase().as_str() { + "M" => Some(HomePageElementType::Media), + "A" => Some(HomePageElementType::Album), + _ => None, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageElementRef { + #[serde(rename = "t")] + element_type: HomePageElementType, + + #[serde(rename = "i")] + id: u64, +} + +impl HomePageElementRef { + pub fn as_string(&self) -> String { + format!("{}{}", self.element_type.as_prefix(), self.id) + } + + pub fn as_list_string(element_refs: &[Self]) -> String { + let elements_strings: Vec<String> = element_refs.iter().map(|r| r.as_string()).collect(); + elements_strings.join(", ") + } + + pub fn from_string(s: &str) -> Option<Self> { + if s.len() < 2 { + return None; + } + + let prefix = s[0..1].to_string(); + let id_str = s[1..].to_string(); + + let element_type = match HomePageElementType::parse_prefix(&prefix) { + Some(t) => t, + None => { + return None; + } + }; + + let id = match id_str.parse::<u64>() { + Ok(i) => i, + Err(_) => { + return None; + } + }; + + Some(Self { element_type, id }) + } + + pub fn from_list_string(list: &str) -> Result<Vec<Self>, usize> { + let list_split: Vec<String> = list.split(",").map(|i| i.trim().to_uppercase()).collect(); + + let mut res: Vec<Self> = Vec::with_capacity(list_split.len()); + + for (i, item) in list_split.iter().enumerate() { + let element_ref = match Self::from_string(item) { + Some(e) => e, + None => { + return Err(i); + } + }; + + res.push(element_ref); + } + + Ok(res) + } + + pub fn from_home_page_elements(elements: &[HomePageElement]) -> Vec<Self> { + elements + .iter() + .filter_map(Self::from_home_page_element) + .collect() + } + + pub fn from_home_page_element(element: &HomePageElement) -> Option<Self> { + match &element.media { + Some(media) => Some(Self { + element_type: HomePageElementType::Media, + id: media.id, + }), + None => element.album.as_ref().map(|album| Self { + element_type: HomePageElementType::Album, + id: album.id, + }), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageGroupSetElementsBody { + #[serde(rename = "elements")] + pub elements: Vec<HomePageElementRef>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageGroupRenameBody { + #[serde(rename = "name")] + pub name: Option<String>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HomePageGroupMoveBody { + #[serde(rename = "position")] + pub position: u32, +} diff --git a/src/models/media.rs b/src/models/media.rs index f62f9eb..714bad8 100644 --- a/src/models/media.rs +++ b/src/models/media.rs @@ -55,9 +55,6 @@ pub struct MediaListItem { #[serde(rename = "title")] pub title: String, - #[serde(rename = "description")] - pub description: String, - #[serde(rename = "thumbnail")] pub thumbnail: Option<String>, @@ -82,9 +79,6 @@ pub struct MediaMetadata { #[serde(rename = "title")] pub title: String, - #[serde(rename = "description")] - pub description: String, - #[serde(rename = "thumbnail")] pub thumbnail: String, @@ -151,8 +145,11 @@ pub struct MediaMetadata { #[serde(rename = "img_notes_url")] pub img_notes_url: Option<String>, - #[serde(rename = "ext_desc_url")] - pub ext_desc_url: Option<String>, + #[serde(rename = "description_url")] + pub description_url: Option<String>, + + #[serde(rename = "related")] + pub related: Option<Vec<MediaListItem>>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -172,9 +169,6 @@ pub struct MediaMetadataExport { #[serde(rename = "title")] pub title: Option<String>, - #[serde(rename = "description")] - pub description: Option<String>, - #[serde(rename = "tags")] pub tags: Option<Vec<String>>, @@ -399,14 +393,8 @@ pub struct MediaUpdateTitleBody { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MediaUpdateDescriptionBody { - #[serde(rename = "description")] - pub description: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct MediaUpdateExtendedDescriptionBody { #[serde(rename = "ext_desc")] - pub ext_desc: String, + pub description: String, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -459,3 +447,9 @@ pub struct MediaRenameSubtitleOrAudioBody { #[serde(rename = "name")] pub name: String, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MediaUpdateRelatedMediaBody { + #[serde(rename = "related")] + pub related: Vec<u64>, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 463b8b3..15e26ca 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -32,3 +32,6 @@ pub use tags::*; mod tasks; pub use tasks::*; + +mod home; +pub use home::*; diff --git a/src/tools/console_table.rs b/src/tools/console_table.rs index 893367b..5aa608c 100644 --- a/src/tools/console_table.rs +++ b/src/tools/console_table.rs @@ -165,7 +165,7 @@ fn sub_string(original_str: &str, skip: usize, limit: usize) -> String { "".to_string() } else if skip == 0 { if limit >= original_str.width() { - return original_str.to_string(); + original_str.to_string() } else { let mut res = "".to_string(); let mut res_width: usize = 0; @@ -181,7 +181,7 @@ fn sub_string(original_str: &str, skip: usize, limit: usize) -> String { res_width += c_width; } - return res; + res } } else { let mut skipped_str = "".to_string(); @@ -202,7 +202,7 @@ fn sub_string(original_str: &str, skip: usize, limit: usize) -> String { skipped_str = original_str.chars().skip(chars_count_skip).collect(); if limit >= skipped_str.width() { - return skipped_str; + skipped_str } else { let mut res = "".to_string(); @@ -217,7 +217,7 @@ fn sub_string(original_str: &str, skip: usize, limit: usize) -> String { res = new_res; } - return res; + res } } }