Compare commits

...

3 commits

Author SHA1 Message Date
5f4115d4ed feat: improved book number and series titles
Some checks failed
Rust / build (map[name:nightly]) (push) Successful in 1m27s
Rust / build (map[name:stable]) (push) Successful in 1m3s
Release Please / Release-plz (push) Failing after 16s
2024-09-25 09:24:43 +01:00
82c6700cd6 feat: add support for --dry-run
Some checks failed
Rust / build (map[name:nightly]) (push) Successful in 1m10s
Rust / build (map[name:stable]) (push) Successful in 2m5s
Release Please / Release-plz (push) Failing after 18s
2024-09-25 09:24:43 +01:00
b109763fe9 build: add forgejo workflow tests
Some checks failed
Rust / build (map[name:nightly]) (push) Successful in 1m39s
Rust / build (map[name:stable]) (push) Successful in 5m25s
Release Please / Release-plz (push) Failing after 22s
2024-09-25 09:24:43 +01:00
7 changed files with 463 additions and 131 deletions

View file

@ -0,0 +1,35 @@
name: Release Please
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- main
env:
CARGO_TERM_COLOR: always
jobs:
release-plz:
name: Release-plz
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run release-plz release-pr
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: release-plz release-pr --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Run release-plz release
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View file

@ -0,0 +1,52 @@
name: Rust
on:
push:
branches: ["next"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: docker
strategy:
matrix:
toolchain:
- name: stable
- name: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check TODOs
uses: https://git.kemitix.net/kemitix/forgejo-todo-checker@v1.1.0
- name: Machete
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: ${{ matrix.toolchain.name }} cargo machete
- name: Format
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
- name: Clippy
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset clippy
- name: Build
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset build
- name: Test
uses: https://git.kemitix.net/kemitix/rust@v2.2.0
with:
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset test

354
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.11"
@ -62,7 +71,9 @@ version = "0.2.0"
dependencies = [
"anyhow",
"clap",
"mp4",
"pretty_assertions",
"regex",
"rstest",
"taglib",
]
@ -73,16 +84,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "byteorder"
version = "1.4.3"
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
@ -130,6 +135,125 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.4.1"
@ -137,10 +261,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "itoa"
version = "1.0.9"
name = "indexmap"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "libc"
@ -149,60 +277,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "mp4"
version = "0.14.0"
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"byteorder",
"bytes",
"num-rational",
"serde",
"serde_json",
"thiserror",
"diff",
"yansi",
]
[[package]]
name = "num-bigint"
version = "0.4.4"
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-bigint",
"num-integer",
"num-traits",
"serde",
]
[[package]]
name = "num-traits"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
"toml_edit",
]
[[package]]
@ -224,40 +332,92 @@ dependencies = [
]
[[package]]
name = "ryu"
version = "1.0.15"
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"serde_derive",
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rstest"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "serde_json"
version = "1.0.105"
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"itoa",
"ryu",
"serde",
"semver",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
@ -297,23 +457,20 @@ dependencies = [
]
[[package]]
name = "thiserror"
version = "1.0.47"
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "thiserror-impl"
version = "1.0.47"
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"proc-macro2",
"quote",
"syn",
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
@ -393,3 +550,18 @@ name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
dependencies = [
"memchr",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

View file

@ -6,7 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.4", features = ["derive", "cargo"] }
mp4 = "0.14.0"
taglib = "1.0.0"
anyhow = "1.0.75"
clap = { version = "4.4", features = ["derive", "cargo"] }
regex = "1.10"
taglib = "1.0.0"
[dev-dependencies]
pretty_assertions = "1.4"
rstest = "0.22"

View file

@ -1,2 +1,5 @@
default:
dry-run:
cargo run -- --dry-run ~/Nextcloud/Audible-Inbox/
run:
cargo run -- ~/Nextcloud/Audible-Inbox/

View file

@ -1,3 +1,8 @@
//
#[cfg(test)]
mod tests;
use anyhow::{Context, Result};
use clap::{crate_version, Parser};
use std::{
@ -17,30 +22,40 @@ struct Arguments {
/// Path
path: Option<String>,
/// Dry-Run
#[clap(long)]
dry_run: bool,
}
impl Arguments {
fn not_dry_run(&self) -> bool {
!self.dry_run
}
}
fn main() {
let args = Arguments::parse();
if args.version {
println!("Version {}", crate_version!());
} else if let Some(path) = args.directory {
rename_files_in_directory(path);
} else if let Some(path) = args.path {
rename_files_in_directory(path);
} else if let Some(path) = &args.directory {
rename_files_in_directory(path.to_string(), &args);
} else if let Some(path) = &args.path {
rename_files_in_directory(path.to_string(), &args);
} else {
eprintln!("Please provide a directory");
}
}
fn rename_files_in_directory(directory: String) {
fn rename_files_in_directory(directory: String, args: &Arguments) {
println!("Renaming files in {}", directory);
match rename_files(&directory, &directory) {
Ok(count) => println!("Renamed {} files", count),
match rename_files(&directory, &directory, args) {
Ok(count) if args.not_dry_run() => println!("Renamed {} files", count),
Ok(_) => println!("Dry-Run complete"),
Err(e) => eprintln!("Error: {}", e),
}
}
fn rename_files(directory: &str, base: &str) -> Result<i32> {
fn rename_files(directory: &str, base: &str, args: &Arguments) -> Result<i32> {
let entries = read_dir(directory).context("Failed to read directory")?;
let mut count = 0;
for entry in entries {
@ -48,42 +63,75 @@ fn rename_files(directory: &str, base: &str) -> Result<i32> {
let path = entry.path();
if path.is_dir() {
if let Some(sub_dir) = path.to_str() {
count += rename_files(sub_dir, base).context("Failed to rename files")?;
count += rename_files(sub_dir, base, args).context("Failed to rename files")?;
}
}
if path.is_file() {
if let Ok(file) = taglib::File::new(&path) {
if let Ok(tag) = file.tag() {
if let Some(title) = tag.title() {
if let Some(artist) = tag.artist() {
let album = parse_album(&title);
count += 1;
let new_name = match album.len() {
0 => format!("{artist}/{title}/{title}.m4b"),
_ => format!("{artist}/{album}/{title}/{title}.m4b"),
};
println!("Renaming {} to {}", path.display(), new_name);
let target_path = Path::new(&base).join(new_name);
if !target_path.exists() {
let dir = target_path.parent().with_context(|| {
format!("Failed to get parent: {:#?}", target_path)
})?;
create_dir_all(dir).with_context(|| {
format!("Failed to create directory: {:#?}", dir)
})?;
}
rename(&path, &target_path).with_context(|| {
format!("Failed to rename file: {:#?}", target_path)
})?;
}
}
}
if !path.is_file() {
continue;
}
let Ok(file) = taglib::File::new(&path) else {
continue;
};
let Ok(tag) = file.tag() else {
continue;
};
let (Some(title), Some(artist)) = (tag.title(), tag.artist()) else {
continue;
};
let album = parse_album(&title);
count += 1;
let new_name = if album.is_empty() {
format!("{artist}/{title}/{title}.m4b")
} else {
build_series_name(&artist, &album, &title)
};
println!("==============================");
println!("- artist: {artist}");
println!("- album : {album}");
println!("- title : {title}");
println!("> {new_name}");
if args.not_dry_run() {
let target_path = Path::new(&base).join(new_name);
if !target_path.exists() {
let dir = target_path
.parent()
.with_context(|| format!("Failed to get parent: {:#?}", target_path))?;
create_dir_all(dir)
.with_context(|| format!("Failed to create directory: {:#?}", dir))?;
}
rename(&path, &target_path)
.with_context(|| format!("Failed to rename file: {:#?}", target_path))?;
}
}
Ok(count)
}
fn build_series_name(artist: &str, album: &str, title: &str) -> String {
let index = parse_index(title);
let title = parse_title(title);
format!("{artist}/{album}/{index}. {title}/{index}. {title}.m4b")
}
fn parse_index(title: &str) -> String {
if let Ok(index_re) = regex::Regex::new(r", Book (?P<INDEX>\d+)") {
if let Some(captures) = index_re.captures(title) {
let index = captures.name("INDEX").map_or("", |m| m.as_str());
return index.to_string();
}
}
String::new()
}
fn parse_title(title: &str) -> String {
if let Ok(title_re) = regex::Regex::new(r"^(?P<TITLE>.*?):") {
if let Some(captures) = title_re.captures(title) {
let title = captures.name("TITLE").map_or("", |m| m.as_str());
return title.to_string();
}
}
title.to_string()
}
fn parse_album(title: &str) -> String {
let parts: Vec<&str> = title.split(':').collect();
match &parts[..] {

18
src/tests.rs Normal file
View file

@ -0,0 +1,18 @@
//
use super::*;
use pretty_assertions::assert_eq;
#[rstest::rstest]
#[case("Discworld", "The Truth: Discworld, Book 25", "25. The Truth")]
#[case(
"A Darker Shade of Magic",
"A Conjuring of Light: A Darker Shade of Magic, Book 3",
"3. A Conjuring of Light"
)]
fn parse_series_details(#[case] album: &str, #[case] title: &str, #[case] expected: &str) {
let expected_result = format!("Bob/{album}/{expected}/{expected}.m4b");
let result = build_series_name("Bob", album, title);
assert_eq!(result, expected_result);
}