You've already forked flix
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9c288d051c
|
|||
|
b518d762f1
|
@@ -0,0 +1,4 @@
|
|||||||
|
allow-expect-in-tests = true
|
||||||
|
check-private-items = true
|
||||||
|
|
||||||
|
allowed-duplicate-crates = []
|
||||||
+21
-9
@@ -1,11 +1,23 @@
|
|||||||
# OS Files
|
*
|
||||||
*~
|
!**/
|
||||||
._*
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Rust
|
!.zed/settings.json
|
||||||
/target
|
!.gitignore
|
||||||
|
|
||||||
# Flix
|
!/CONTRIBUTING.md
|
||||||
flix.db
|
!/LICENSE.md
|
||||||
flix.redb
|
!/README.md
|
||||||
|
|
||||||
|
!/.clippy.toml
|
||||||
|
!/.rustfmt.toml
|
||||||
|
!/.tombi.toml
|
||||||
|
!/Cargo.toml
|
||||||
|
!/Cargo.lock
|
||||||
|
!/justfile
|
||||||
|
!/rust-toolchain.toml
|
||||||
|
|
||||||
|
!/crates/**/*.rs
|
||||||
|
!/crates/**/Cargo.toml
|
||||||
|
!/crates/**/README.md
|
||||||
|
|
||||||
|
!/lints/*.toml
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
hard_tabs = true
|
hard_tabs = true
|
||||||
wrap_comments = true
|
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
[format.rules]
|
||||||
|
indent-style = "tab"
|
||||||
|
indent-width = 4
|
||||||
|
line-width = 120
|
||||||
|
|
||||||
|
# Disable TOML 1.1 features from tombi as long as we support rust <1.94
|
||||||
|
[[schemas]]
|
||||||
|
toml-version = "v1.0.0"
|
||||||
|
path = "tombi://www.schemastore.org/cargo.json"
|
||||||
|
include = ["Cargo.toml"]
|
||||||
+7
-6
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"languages": {
|
|
||||||
"TOML": {
|
|
||||||
"format_on_save": "on",
|
|
||||||
"formatter": { "language_server": { "name": "tombi" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"lsp": {
|
"lsp": {
|
||||||
|
"just-lsp": {
|
||||||
|
"initialization_options": {
|
||||||
|
"formatting": {
|
||||||
|
"indentation": "\t",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"rust-analyzer": {
|
"rust-analyzer": {
|
||||||
"initialization_options": {
|
"initialization_options": {
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
Generated
+608
-991
File diff suppressed because it is too large
Load Diff
+555
-59
@@ -1,74 +1,570 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "3"
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
license-file = "LICENSE.md"
|
|
||||||
|
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.89.0"
|
rust-version = "1.94"
|
||||||
|
license-file = "LICENSE.md"
|
||||||
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
flix = { path = "crates/flix", version = "=0.0.20", default-features = false }
|
flix = { path = "crates/flix", version = "=0.1.0", default-features = false }
|
||||||
flix-cli = { path = "crates/cli", version = "=0.0.20", default-features = false }
|
flix-cli = { path = "crates/cli", version = "=0.1.0", default-features = false }
|
||||||
flix-db = { path = "crates/db", version = "=0.0.20", default-features = false }
|
flix-db = { path = "crates/db", version = "=0.1.0", default-features = false }
|
||||||
flix-fs = { path = "crates/fs", version = "=0.0.20", default-features = false }
|
flix-fs = { path = "crates/fs", version = "=0.1.0", default-features = false }
|
||||||
flix-model = { path = "crates/model", version = "=0.0.20", default-features = false }
|
flix-model = { path = "crates/model", version = "=0.1.0", default-features = false }
|
||||||
flix-mux = { path = "crates/cli-mux", version = "=0.0.20", default-features = false }
|
flix-mux = { path = "crates/cli-mux", version = "=0.1.0", default-features = false }
|
||||||
flix-tmdb = { path = "crates/tmdb", version = "=0.0.20", default-features = false }
|
flix-tmdb = { path = "crates/tmdb", version = "=0.1.0", default-features = false }
|
||||||
|
|
||||||
seamantic = { version = "^0.0.14", default-features = false }
|
anyhow = { version = "^1.0.87", default-features = false }
|
||||||
|
async-stream = { version = "^0.3.5", default-features = false }
|
||||||
sea-orm = { version = "=2.0.0-rc.38", default-features = false }
|
bytes = { version = "^1.7.1", default-features = false }
|
||||||
sea-orm-migration = { version = "=2.0.0-rc.38", default-features = false }
|
chrono = { version = "^0.4.40", default-features = false }
|
||||||
|
clap = { version = "^4.3.2", default-features = false }
|
||||||
anyhow = { version = "^1", default-features = false }
|
console = { version = "^0.16.0", default-features = false }
|
||||||
async-stream = { version = "^0.3", default-features = false }
|
either = { version = "^1.6.1", default-features = false }
|
||||||
bytes = { version = "^1", default-features = false }
|
governor = { version = "^0.10.0", default-features = false }
|
||||||
chrono = { version = "^0.4", default-features = false }
|
indicatif = { version = "^0.18.0", default-features = false }
|
||||||
clap = { version = "^4", default-features = false }
|
itertools = { version = "^0.15.0", default-features = false }
|
||||||
console = { version = "^0.16", default-features = false }
|
nonzero_ext = { version = "^0.3.0", default-features = false }
|
||||||
dialoguer = { version = "^0.12", default-features = false }
|
redb = { version = "^4.0.0", default-features = false }
|
||||||
either = { version = "^1", default-features = false }
|
regex = { version = "^1.11.2", default-features = false }
|
||||||
futures = { version = "^0.3", default-features = false }
|
reqwest = { version = "^0.13.0", default-features = false }
|
||||||
governor = { version = "^0.10", default-features = false }
|
sea-orm = { version = "^2.0.1", default-features = false }
|
||||||
indicatif = { version = "^0.18", default-features = false }
|
sea-orm-migration = { version = "^2.0.0", default-features = false }
|
||||||
itertools = { version = "^0.14", default-features = false }
|
seamantic = { version = "^1.0.1", default-features = false }
|
||||||
nonzero_ext = { version = "^0.3", default-features = false }
|
serde = { version = "^1.0.225", default-features = false }
|
||||||
redb = { version = "^4", default-features = false }
|
serde_json = { version = "^1.0.142", default-features = false }
|
||||||
regex = { version = "^1", default-features = false }
|
serde_test = { version = "^1.0.0", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
tempfile = { version = "^3.0.0", default-features = false }
|
||||||
serde = { version = "^1", default-features = false }
|
thiserror = { version = "^2.0.18", default-features = false }
|
||||||
serde_json = { version = "^1", default-features = false }
|
tokio = { version = "^1.48.0", default-features = false }
|
||||||
serde_test = { version = "^1", default-features = false }
|
tokio-stream = { version = "^0.1.8", default-features = false }
|
||||||
thiserror = { version = "^2", default-features = false }
|
toml = { version = "^1.0.0", default-features = false }
|
||||||
tokio = { version = "^1", default-features = false }
|
tracing = { version = "^0.1.37", default-features = false }
|
||||||
tokio-stream = { version = "^0.1", default-features = false }
|
tracing-subscriber = { version = "^0.3.17", default-features = false }
|
||||||
toml = { version = "^1", default-features = false }
|
url = { version = "^2.4.0", default-features = false }
|
||||||
tracing = { version = "^0.1", default-features = false }
|
url-macro = { version = "^0.2.0", default-features = false }
|
||||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
walkdir = { version = "^2.0.0", default-features = false }
|
||||||
url = { version = "^2", default-features = false }
|
|
||||||
url-macro = { version = "^0.2", default-features = false }
|
|
||||||
walkdir = { version = "^2", default-features = false }
|
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
arithmetic_side_effects = "forbid"
|
cargo = { level = "warn", priority = -1 }
|
||||||
as_conversions = "forbid"
|
complexity = { level = "warn", priority = -1 }
|
||||||
checked_conversions = "forbid"
|
correctness = { level = "deny", priority = -1 }
|
||||||
default_union_representation = "forbid"
|
perf = { level = "warn", priority = -1 }
|
||||||
expect_used = "forbid"
|
style = { level = "warn", priority = -1 }
|
||||||
indexing_slicing = "forbid"
|
suspicious = { level = "warn", priority = -1 }
|
||||||
integer_division = "forbid"
|
|
||||||
integer_division_remainder_used = "forbid"
|
absolute-paths = "allow"
|
||||||
transmute_undefined_repr = "forbid"
|
alloc-instead-of-core = "warn"
|
||||||
unchecked_time_subtraction = "forbid"
|
allow-attributes = "warn"
|
||||||
unwrap_used = "forbid"
|
allow-attributes-without-reason = "deny"
|
||||||
|
arbitrary-source-item-ordering = "allow"
|
||||||
|
arithmetic-side-effects = "deny"
|
||||||
|
as-conversions = "deny"
|
||||||
|
as-pointer-underscore = "deny"
|
||||||
|
as-ptr-cast-mut = "deny"
|
||||||
|
as-underscore = "deny"
|
||||||
|
assertions-on-result-states = "warn"
|
||||||
|
assigning-clones = "warn"
|
||||||
|
big-endian-bytes = "allow"
|
||||||
|
bool-to-int-with-if = "warn"
|
||||||
|
borrow-as-ptr = "warn"
|
||||||
|
branches-sharing-code = "warn"
|
||||||
|
case-sensitive-file-extension-comparisons = "warn"
|
||||||
|
cast-lossless = "deny"
|
||||||
|
cast-possible-truncation = "deny"
|
||||||
|
cast-possible-wrap = "deny"
|
||||||
|
cast-precision-loss = "deny"
|
||||||
|
cast-ptr-alignment = "deny"
|
||||||
|
cast-sign-loss = "deny"
|
||||||
|
cfg-not-test = "deny"
|
||||||
|
checked-conversions = "deny"
|
||||||
|
clear-with-drain = "warn"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
cloned-instead-of-copied = "warn"
|
||||||
|
coerce-container-to-any = "warn"
|
||||||
|
cognitive-complexity = "allow"
|
||||||
|
collapsible-else-if = "warn"
|
||||||
|
collection-is-never-read = "warn"
|
||||||
|
comparison-chain = "warn"
|
||||||
|
copy-iterator = "warn"
|
||||||
|
create-dir = "warn"
|
||||||
|
dbg-macro = "warn"
|
||||||
|
debug-assert-with-mut-call = "deny"
|
||||||
|
decimal-bitwise-operands = "warn"
|
||||||
|
decimal-literal-representation = "warn"
|
||||||
|
default-numeric-fallback = "warn"
|
||||||
|
default-trait-access = "warn"
|
||||||
|
default-union-representation = "allow"
|
||||||
|
deref-by-slicing = "warn"
|
||||||
|
derive-partial-eq-without-eq = "warn"
|
||||||
|
disallowed-script-idents = "deny"
|
||||||
|
doc-broken-link = "warn"
|
||||||
|
doc-comment-double-space-linebreaks = "warn"
|
||||||
|
doc-include-without-cfg = "warn"
|
||||||
|
doc-link-code = "warn"
|
||||||
|
doc-link-with-quotes = "warn"
|
||||||
|
doc-markdown = "warn"
|
||||||
|
doc-paragraphs-missing-punctuation = "warn"
|
||||||
|
duration-suboptimal-units = "warn"
|
||||||
|
elidable-lifetime-names = "warn"
|
||||||
|
else-if-without-else = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
empty-enum-variants-with-brackets = "warn"
|
||||||
|
empty-enums = "warn"
|
||||||
|
empty-structs-with-brackets = "warn"
|
||||||
|
enum-glob-use = "warn"
|
||||||
|
equatable-if-let = "warn"
|
||||||
|
error-impl-error = "warn"
|
||||||
|
exhaustive-enums = "warn"
|
||||||
|
exhaustive-structs = "warn"
|
||||||
|
exit = "warn"
|
||||||
|
expect-used = "warn"
|
||||||
|
expl-impl-clone-on-copy = "warn"
|
||||||
|
explicit-deref-methods = "warn"
|
||||||
|
explicit-into-iter-loop = "warn"
|
||||||
|
explicit-iter-loop = "warn"
|
||||||
|
fallible-impl-from = "warn"
|
||||||
|
field-scoped-visibility-modifiers = "warn"
|
||||||
|
filetype-is-file = "warn"
|
||||||
|
filter-map-next = "warn"
|
||||||
|
flat-map-option = "warn"
|
||||||
|
float-arithmetic = "warn"
|
||||||
|
float-cmp = "warn"
|
||||||
|
float-cmp-const = "warn"
|
||||||
|
fn-params-excessive-bools = "allow"
|
||||||
|
fn-to-numeric-cast-any = "warn"
|
||||||
|
format-collect = "warn"
|
||||||
|
format-push-string = "warn"
|
||||||
|
future-not-send = "warn"
|
||||||
|
get-unwrap = "warn"
|
||||||
|
host-endian-bytes = "allow"
|
||||||
|
if-not-else = "warn"
|
||||||
|
if-then-some-else-none = "warn"
|
||||||
|
ignore-without-reason = "warn"
|
||||||
|
ignored-unit-patterns = "warn"
|
||||||
|
impl-trait-in-params = "warn"
|
||||||
|
implicit-clone = "warn"
|
||||||
|
implicit-hasher = "warn"
|
||||||
|
implicit-return = "allow"
|
||||||
|
imprecise-flops = "warn"
|
||||||
|
inconsistent-struct-constructor = "warn"
|
||||||
|
index-refutable-slice = "deny"
|
||||||
|
indexing-slicing = "deny"
|
||||||
|
inefficient-to-string = "warn"
|
||||||
|
infinite-loop = "warn"
|
||||||
|
inline-always = "warn"
|
||||||
|
inline-asm-x86-att-syntax = "warn"
|
||||||
|
inline-asm-x86-intel-syntax = "warn"
|
||||||
|
inline-modules = "allow"
|
||||||
|
inline-trait-bounds = "allow"
|
||||||
|
integer-division = "deny"
|
||||||
|
integer-division-remainder-used = "deny"
|
||||||
|
into-iter-without-iter = "warn"
|
||||||
|
invalid-upcast-comparisons = "warn"
|
||||||
|
ip-constant = "warn"
|
||||||
|
items-after-statements = "warn"
|
||||||
|
iter-filter-is-ok = "warn"
|
||||||
|
iter-filter-is-some = "warn"
|
||||||
|
iter-not-returning-iterator = "warn"
|
||||||
|
iter-on-empty-collections = "warn"
|
||||||
|
iter-on-single-items = "warn"
|
||||||
|
iter-over-hash-type = "warn"
|
||||||
|
iter-with-drain = "warn"
|
||||||
|
iter-without-into-iter = "warn"
|
||||||
|
large-digit-groups = "warn"
|
||||||
|
large-futures = "warn"
|
||||||
|
large-include-file = "warn"
|
||||||
|
large-stack-arrays = "warn"
|
||||||
|
large-stack-frames = "warn"
|
||||||
|
large-types-passed-by-value = "warn"
|
||||||
|
let-underscore-must-use = "warn"
|
||||||
|
let-underscore-untyped = "warn"
|
||||||
|
linkedlist = "warn"
|
||||||
|
literal-string-with-formatting-args = "warn"
|
||||||
|
little-endian-bytes = "allow"
|
||||||
|
lossy-float-literal = "warn"
|
||||||
|
macro-use-imports = "warn"
|
||||||
|
manual-assert = "warn"
|
||||||
|
manual-assert-eq = "warn"
|
||||||
|
manual-ilog2 = "warn"
|
||||||
|
manual-instant-elapsed = "warn"
|
||||||
|
manual-is-power-of-two = "warn"
|
||||||
|
manual-is-variant-and = "warn"
|
||||||
|
manual-let-else = "warn"
|
||||||
|
manual-midpoint = "warn"
|
||||||
|
manual-string-new = "warn"
|
||||||
|
many-single-char-names = "warn"
|
||||||
|
map-err-ignore = "warn"
|
||||||
|
map-unwrap-or = "warn"
|
||||||
|
map-with-unused-argument-over-ranges = "warn"
|
||||||
|
match-bool = "warn"
|
||||||
|
match-same-arms = "warn"
|
||||||
|
match-wild-err-arm = "warn"
|
||||||
|
match-wildcard-for-single-variants = "warn"
|
||||||
|
maybe-infinite-iter = "warn"
|
||||||
|
mem-forget = "warn"
|
||||||
|
min-ident-chars = "allow"
|
||||||
|
mismatching-type-param-order = "warn"
|
||||||
|
missing-assert-message = "warn"
|
||||||
|
missing-asserts-for-indexing = "warn"
|
||||||
|
missing-const-for-fn = "warn"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
missing-errors-doc = "warn"
|
||||||
|
missing-fields-in-debug = "warn"
|
||||||
|
missing-inline-in-public-items = "warn"
|
||||||
|
missing-panics-doc = "warn"
|
||||||
|
missing-trait-methods = "warn"
|
||||||
|
mixed-read-write-in-expression = "warn"
|
||||||
|
mod-module-files = "allow"
|
||||||
|
module-name-repetitions = "warn"
|
||||||
|
modulo-arithmetic = "warn"
|
||||||
|
multiple-crate-versions = "allow"
|
||||||
|
multiple-inherent-impl = "warn"
|
||||||
|
multiple-unsafe-ops-per-block = "warn"
|
||||||
|
must-use-candidate = "warn"
|
||||||
|
mut-mut = "warn"
|
||||||
|
mutex-atomic = "warn"
|
||||||
|
mutex-integer = "warn"
|
||||||
|
naive-bytecount = "warn"
|
||||||
|
needless-bitwise-bool = "warn"
|
||||||
|
needless-collect = "warn"
|
||||||
|
needless-continue = "warn"
|
||||||
|
needless-for-each = "warn"
|
||||||
|
needless-pass-by-ref-mut = "warn"
|
||||||
|
needless-pass-by-value = "warn"
|
||||||
|
needless-raw-string-hashes = "warn"
|
||||||
|
needless-raw-strings = "warn"
|
||||||
|
needless-type-cast = "warn"
|
||||||
|
no-effect-underscore-binding = "warn"
|
||||||
|
no-mangle-with-rust-abi = "warn"
|
||||||
|
non-ascii-literal = "allow"
|
||||||
|
non-send-fields-in-send-ty = "warn"
|
||||||
|
non-std-lazy-statics = "warn"
|
||||||
|
non-zero-suggestions = "warn"
|
||||||
|
nonminimal-bool = "warn"
|
||||||
|
nonstandard-macro-braces = "warn"
|
||||||
|
option-as-ref-cloned = "warn"
|
||||||
|
option-if-let-else = "warn"
|
||||||
|
option-option = "warn"
|
||||||
|
or-fun-call = "warn"
|
||||||
|
overly-complex-bool-expr = "allow"
|
||||||
|
panic = "warn"
|
||||||
|
panic-in-result-fn = "warn"
|
||||||
|
partial-pub-fields = "warn"
|
||||||
|
path-buf-push-overwrite = "warn"
|
||||||
|
pathbuf-init-then-push = "warn"
|
||||||
|
pattern-type-mismatch = "allow"
|
||||||
|
pointer-format = "warn"
|
||||||
|
precedence-bits = "warn"
|
||||||
|
print-stderr = "warn"
|
||||||
|
print-stdout = "warn"
|
||||||
|
ptr-as-ptr = "warn"
|
||||||
|
ptr-cast-constness = "warn"
|
||||||
|
ptr-offset-by-literal = "warn"
|
||||||
|
pub-underscore-fields = "warn"
|
||||||
|
pub-use = "allow"
|
||||||
|
pub-with-shorthand = "allow"
|
||||||
|
pub-without-shorthand = "warn"
|
||||||
|
question-mark-used = "allow"
|
||||||
|
range-minus-one = "warn"
|
||||||
|
range-plus-one = "warn"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "warn"
|
||||||
|
read-zero-byte-vec = "warn"
|
||||||
|
redundant-clone = "warn"
|
||||||
|
redundant-closure-for-method-calls = "warn"
|
||||||
|
redundant-else = "warn"
|
||||||
|
redundant-pub-crate = "allow"
|
||||||
|
redundant-test-prefix = "warn"
|
||||||
|
redundant-type-annotations = "warn"
|
||||||
|
ref-as-ptr = "warn"
|
||||||
|
ref-binding-to-reference = "warn"
|
||||||
|
ref-option = "warn"
|
||||||
|
ref-option-ref = "warn"
|
||||||
|
ref-patterns = "warn"
|
||||||
|
renamed-function-params = "warn"
|
||||||
|
rest-pat-in-fully-bound-structs = "warn"
|
||||||
|
return-and-then = "warn"
|
||||||
|
return-self-not-must-use = "warn"
|
||||||
|
same-functions-in-if-condition = "warn"
|
||||||
|
same-length-and-capacity = "warn"
|
||||||
|
same-name-method = "warn"
|
||||||
|
search-is-some = "warn"
|
||||||
|
self-named-module-files = "warn"
|
||||||
|
self-only-used-in-recursion = "warn"
|
||||||
|
semicolon-if-nothing-returned = "warn"
|
||||||
|
semicolon-inside-block = "warn"
|
||||||
|
semicolon-outside-block = "allow"
|
||||||
|
separated-literal-suffix = "allow"
|
||||||
|
set-contains-or-insert = "warn"
|
||||||
|
shadow-reuse = "allow"
|
||||||
|
shadow-same = "allow"
|
||||||
|
shadow-unrelated = "allow"
|
||||||
|
should-panic-without-expect = "warn"
|
||||||
|
significant-drop-in-scrutinee = "warn"
|
||||||
|
significant-drop-tightening = "warn"
|
||||||
|
similar-names = "allow"
|
||||||
|
single-call-fn = "allow"
|
||||||
|
single-char-lifetime-names = "allow"
|
||||||
|
single-char-pattern = "allow"
|
||||||
|
single-match-else = "warn"
|
||||||
|
single-option-map = "warn"
|
||||||
|
stable-sort-primitive = "warn"
|
||||||
|
std-instead-of-alloc = "warn"
|
||||||
|
std-instead-of-core = "warn"
|
||||||
|
str-split-at-newline = "warn"
|
||||||
|
str-to-string = "warn"
|
||||||
|
string-add = "warn"
|
||||||
|
string-add-assign = "warn"
|
||||||
|
string-lit-as-bytes = "warn"
|
||||||
|
string-lit-chars-any = "warn"
|
||||||
|
string-slice = "warn"
|
||||||
|
struct-excessive-bools = "allow"
|
||||||
|
struct-field-names = "warn"
|
||||||
|
suboptimal-flops = "warn"
|
||||||
|
suspicious-operation-groupings = "warn"
|
||||||
|
suspicious-xor-used-as-pow = "warn"
|
||||||
|
tests-outside-test-module = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
too-long-first-doc-paragraph = "allow"
|
||||||
|
too-many-lines = "allow"
|
||||||
|
trailing-empty-array = "warn"
|
||||||
|
trait-duplication-in-bounds = "warn"
|
||||||
|
transmute-ptr-to-ptr = "warn"
|
||||||
|
transmute-undefined-repr = "warn"
|
||||||
|
trivial-regex = "warn"
|
||||||
|
trivially-copy-pass-by-ref = "warn"
|
||||||
|
try-err = "warn"
|
||||||
|
tuple-array-conversions = "warn"
|
||||||
|
type-repetition-in-bounds = "warn"
|
||||||
|
unchecked-time-subtraction = "deny"
|
||||||
|
undocumented-unsafe-blocks = "deny"
|
||||||
|
unicode-not-nfc = "warn"
|
||||||
|
unimplemented = "warn"
|
||||||
|
uninhabited-references = "warn"
|
||||||
|
uninlined-format-args = "warn"
|
||||||
|
unnecessary-box-returns = "warn"
|
||||||
|
unnecessary-debug-formatting = "warn"
|
||||||
|
unnecessary-join = "warn"
|
||||||
|
unnecessary-literal-bound = "warn"
|
||||||
|
unnecessary-safety-comment = "warn"
|
||||||
|
unnecessary-safety-doc = "warn"
|
||||||
|
unnecessary-self-imports = "warn"
|
||||||
|
unnecessary-semicolon = "warn"
|
||||||
|
unnecessary-struct-initialization = "warn"
|
||||||
|
unnecessary-trailing-comma = "warn"
|
||||||
|
unnecessary-wraps = "warn"
|
||||||
|
unneeded-field-pattern = "warn"
|
||||||
|
unnested-or-patterns = "warn"
|
||||||
|
unreachable = "warn"
|
||||||
|
unreadable-literal = "warn"
|
||||||
|
unsafe-derive-deserialize = "warn"
|
||||||
|
unseparated-literal-suffix = "warn"
|
||||||
|
unused-async = "warn"
|
||||||
|
unused-async-trait-impl = "warn"
|
||||||
|
unused-peekable = "warn"
|
||||||
|
unused-result-ok = "warn"
|
||||||
|
unused-rounding = "warn"
|
||||||
|
unused-self = "warn"
|
||||||
|
unused-trait-names = "warn"
|
||||||
|
unwrap-in-result = "warn"
|
||||||
|
unwrap-used = "warn"
|
||||||
|
use-debug = "warn"
|
||||||
|
use-self = "warn"
|
||||||
|
used-underscore-binding = "warn"
|
||||||
|
used-underscore-items = "warn"
|
||||||
|
useless-let-if-seq = "warn"
|
||||||
|
verbose-bit-mask = "warn"
|
||||||
|
verbose-file-reads = "warn"
|
||||||
|
volatile-composites = "warn"
|
||||||
|
while-float = "warn"
|
||||||
|
wildcard-enum-match-arm = "warn"
|
||||||
|
wildcard-imports = "warn"
|
||||||
|
with-capacity-zero = "warn"
|
||||||
|
zero-sized-map-values = "warn"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
arithmetic_overflow = "forbid"
|
deprecated-safe = "forbid"
|
||||||
missing_docs = "forbid"
|
future-incompatible = "deny"
|
||||||
unsafe_code = "forbid"
|
keyword-idents = "deny"
|
||||||
unused_doc_comments = "forbid"
|
let-underscore = "warn"
|
||||||
|
nonstandard-style = "warn"
|
||||||
|
refining-impl-trait = "warn"
|
||||||
|
rust-2018-compatibility = "deny"
|
||||||
|
rust-2018-idioms = "deny"
|
||||||
|
rust-2021-compatibility = "deny"
|
||||||
|
rust-2024-compatibility = "deny"
|
||||||
|
unknown-or-malformed-diagnostic-attributes = "deny"
|
||||||
|
unused = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
ambiguous-glob-reexports = "warn"
|
||||||
|
ambiguous-negative-literals = "deny"
|
||||||
|
ambiguous-wide-pointer-comparisons = "warn"
|
||||||
|
arithmetic-overflow = "deny"
|
||||||
|
asm-sub-register = "warn"
|
||||||
|
async-fn-in-trait = "warn"
|
||||||
|
bad-asm-style = "warn"
|
||||||
|
binary-asm-labels = "deny"
|
||||||
|
bindings-with-variant-name = "deny"
|
||||||
|
break-with-label-and-loop = "warn"
|
||||||
|
c-void-returns = "warn"
|
||||||
|
clashing-extern-declarations = "warn"
|
||||||
|
closure-returning-async-block = "warn"
|
||||||
|
confusable-idents = "warn"
|
||||||
|
const-item-interior-mutations = "warn"
|
||||||
|
const-item-mutation = "warn"
|
||||||
|
dangerous-implicit-autorefs = "deny"
|
||||||
|
dangling-pointers-from-locals = "warn"
|
||||||
|
dangling-pointers-from-temporaries = "warn"
|
||||||
|
dead-code-pub-in-binary = "warn"
|
||||||
|
deprecated = "warn"
|
||||||
|
deprecated-in-future = "allow"
|
||||||
|
deprecated-where-clause-location = "warn"
|
||||||
|
deref-into-dyn-supertrait = "warn"
|
||||||
|
deref-nullptr = "deny"
|
||||||
|
double-negations = "warn"
|
||||||
|
drop-bounds = "warn"
|
||||||
|
dropping-copy-types = "warn"
|
||||||
|
dropping-references = "warn"
|
||||||
|
duplicate-features = "deny"
|
||||||
|
duplicate-macro-attributes = "warn"
|
||||||
|
dyn-drop = "warn"
|
||||||
|
enum-intrinsics-non-enums = "deny"
|
||||||
|
explicit-builtin-cfgs-in-flags = "deny"
|
||||||
|
exported-private-dependencies = "warn"
|
||||||
|
ffi-unwind-calls = "warn"
|
||||||
|
for-loops-over-fallibles = "warn"
|
||||||
|
forgetting-copy-types = "warn"
|
||||||
|
forgetting-references = "warn"
|
||||||
|
function-casts-as-integer = "warn"
|
||||||
|
function-item-references = "warn"
|
||||||
|
hidden-glob-reexports = "warn"
|
||||||
|
impl-trait-redundant-captures = "warn"
|
||||||
|
improper-ctypes = "warn"
|
||||||
|
improper-ctypes-definitions = "warn"
|
||||||
|
improper-gpu-kernel-arg = "warn"
|
||||||
|
incomplete-features = "warn"
|
||||||
|
incomplete-include = "deny"
|
||||||
|
ineffective-unstable-trait-impl = "deny"
|
||||||
|
inline-no-sanitize = "warn"
|
||||||
|
integer-to-ptr-transmutes = "warn"
|
||||||
|
internal-features = "warn"
|
||||||
|
invalid-atomic-ordering = "deny"
|
||||||
|
invalid-doc-attributes = "warn"
|
||||||
|
invalid-from-utf8 = "warn"
|
||||||
|
invalid-from-utf8-unchecked = "deny"
|
||||||
|
invalid-nan-comparisons = "warn"
|
||||||
|
invalid-null-arguments = "deny"
|
||||||
|
invalid-reference-casting = "deny"
|
||||||
|
invalid-runtime-symbol-definitions = "deny"
|
||||||
|
invalid-value = "warn"
|
||||||
|
irrefutable-let-patterns = "warn"
|
||||||
|
large-assignments = "warn"
|
||||||
|
linker-info = "allow"
|
||||||
|
linker-messages = "warn"
|
||||||
|
long-running-const-eval = "deny"
|
||||||
|
macro-use-extern-crate = "warn"
|
||||||
|
meta-variable-misuse = "warn"
|
||||||
|
mismatched-lifetime-syntaxes = "warn"
|
||||||
|
missing-abi = "warn"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "warn"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-gpu-kernel-export-name = "warn"
|
||||||
|
mixed-script-confusables = "warn"
|
||||||
|
mutable-transmutes = "deny"
|
||||||
|
named-arguments-used-positionally = "warn"
|
||||||
|
named-asm-labels = "deny"
|
||||||
|
no-mangle-const-items = "deny"
|
||||||
|
no-mangle-generic-items = "warn"
|
||||||
|
non-ascii-idents = "deny"
|
||||||
|
non-contiguous-range-endpoints = "warn"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
non-shorthand-field-patterns = "warn"
|
||||||
|
noop-method-call = "warn"
|
||||||
|
opaque-hidden-inferred-bound = "warn"
|
||||||
|
overflowing-literals = "deny"
|
||||||
|
overlapping-range-endpoints = "warn"
|
||||||
|
private-bounds = "warn"
|
||||||
|
private-interfaces = "warn"
|
||||||
|
ptr-to-integer-transmute-in-consts = "warn"
|
||||||
|
redundant-imports = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
renamed-and-removed-lints = "warn"
|
||||||
|
rtsan-nonblocking-async = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
special-module-name = "warn"
|
||||||
|
stable-features = "warn"
|
||||||
|
suspicious-double-ref-op = "warn"
|
||||||
|
suspicious-runtime-symbol-definitions = "warn"
|
||||||
|
text-direction-codepoint-in-comment = "deny"
|
||||||
|
text-direction-codepoint-in-literal = "deny"
|
||||||
|
trivial-bounds = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
type-alias-bounds = "warn"
|
||||||
|
uncommon-codepoints = "warn"
|
||||||
|
unconditional-panic = "deny"
|
||||||
|
unconditional-recursion = "warn"
|
||||||
|
undropped-manually-drops = "deny"
|
||||||
|
unexpected-cfgs = "warn"
|
||||||
|
unfulfilled-lint-expectations = "warn"
|
||||||
|
ungated-async-fn-track-caller = "warn"
|
||||||
|
unit-bindings = "warn"
|
||||||
|
unknown-crate-types = "deny"
|
||||||
|
unknown-lints = "warn"
|
||||||
|
unnameable-test-items = "warn"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unnecessary-transmutes = "warn"
|
||||||
|
unpredictable-function-pointer-comparisons = "warn"
|
||||||
|
unreachable-cfg-select-predicates = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unstable-features = "forbid"
|
||||||
|
unused-associated-type-bounds = "warn"
|
||||||
|
unused-comparisons = "warn"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
unused-results = "warn"
|
||||||
|
useless-deprecated = "deny"
|
||||||
|
useless-ptr-null-checks = "warn"
|
||||||
|
uses-power-alignment = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
warnings = "warn"
|
||||||
|
while-true = "warn"
|
||||||
|
|
||||||
|
# unstable lints
|
||||||
|
# default-overrides-default-fields = "deny"
|
||||||
|
# deprecated-llvm-intrinsic = "warn"
|
||||||
|
# implicit-provenance-casts = "deny"
|
||||||
|
# multiple-supertrait-upcastable = "warn"
|
||||||
|
# must-not-suspend = "warn"
|
||||||
|
# non-exhaustive-omitted-patterns = "warn"
|
||||||
|
# resolving-to-items-shadowing-supertrait-items = "warn"
|
||||||
|
# shadowing-supertrait-items = "warn"
|
||||||
|
# tail-call-track-caller = "warn"
|
||||||
|
# test-unstable-lint = "deny"
|
||||||
|
# unqualified-local-imports = "deny"
|
||||||
|
|
||||||
|
[workspace.lints.rustdoc]
|
||||||
|
bare-urls = "warn"
|
||||||
|
broken-intra-doc-links = "warn"
|
||||||
|
invalid-codeblock-attributes = "warn"
|
||||||
|
invalid-html-tags = "warn"
|
||||||
|
invalid-rust-codeblocks = "warn"
|
||||||
|
missing-crate-level-docs = "warn"
|
||||||
|
private-doc-tests = "warn"
|
||||||
|
private-intra-doc-links = "warn"
|
||||||
|
redundant-explicit-links = "warn"
|
||||||
|
unescaped-backticks = "warn"
|
||||||
|
|
||||||
|
# unstable lints
|
||||||
|
# missing-doc-code-examples = "warn"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
Libraries and tools for dealing with media metadata
|
Libraries and tools for dealing with media metadata
|
||||||
|
|
||||||
## Various builds
|
## Development
|
||||||
|
|
||||||
- build: `cargo hack --feature-powerset build`
|
Run the full validation suite before committing:
|
||||||
- clippy: `cargo hack --feature-powerset clippy -- -D warnings`
|
|
||||||
- test: `cargo hack --feature-powerset test`
|
```sh
|
||||||
- test old: `cargo +1.89 hack --feature-powerset test`
|
just all
|
||||||
- fmt: `cargo fmt --check`
|
just install
|
||||||
- docs: `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features`
|
```
|
||||||
- install: `cargo install --path crates/cli`
|
|
||||||
- install: `cargo install --path crates/cli-mux`
|
##### Publish
|
||||||
- semver: `cargo semver-checks --all-features`
|
|
||||||
- publish: `cargo publish --dry-run --workspace`
|
- `cargo semver-checks --all-features`
|
||||||
|
- `cargo publish --dry-run -p <crate>`
|
||||||
|
- `cargo publish --dry-run --workspace`
|
||||||
|
|
||||||
## Building flix.db
|
## Building flix.db
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ run flix add tmdb movie 1039773 # The American Society of Magic
|
|||||||
run flix add tmdb movie 8916 # Antz (1998)
|
run flix add tmdb movie 8916 # Antz (1998)
|
||||||
run flix add tmdb movie 1072790 # Anyone But You (2023)
|
run flix add tmdb movie 1072790 # Anyone But You (2023)
|
||||||
run flix add tmdb movie 339403 # Baby Driver (2017)
|
run flix add tmdb movie 339403 # Baby Driver (2017)
|
||||||
|
run flix add tmdb movie 1083381 # Backrooms (2026)
|
||||||
run flix add tmdb movie 8848 # The Bank Job (2008)
|
run flix add tmdb movie 8848 # The Bank Job (2008)
|
||||||
run flix add tmdb movie 5559 # Bee Movie (2007)
|
run flix add tmdb movie 5559 # Bee Movie (2007)
|
||||||
run flix add tmdb movie 401123 # Beta Test (2016)
|
run flix add tmdb movie 401123 # Beta Test (2016)
|
||||||
@@ -22,6 +23,7 @@ run flix add tmdb movie 937287 # Challengers (2024)
|
|||||||
run flix add tmdb movie 5289 # Chaos (2005)
|
run flix add tmdb movie 5289 # Chaos (2005)
|
||||||
run flix add tmdb movie 118 # Charlie and the Chocolate Factory (2005)
|
run flix add tmdb movie 118 # Charlie and the Chocolate Factory (2005)
|
||||||
run flix add tmdb movie 9982 # Chicken Little (2005)
|
run flix add tmdb movie 9982 # Chicken Little (2005)
|
||||||
|
run flix add tmdb movie 1413976 # Citizen Vigilante (2026)
|
||||||
run flix add tmdb movie 15028 # Clockstoppers (2002)
|
run flix add tmdb movie 15028 # Clockstoppers (2002)
|
||||||
run flix add tmdb movie 1538 # Collateral (2004)
|
run flix add tmdb movie 1538 # Collateral (2004)
|
||||||
run flix add tmdb movie 9612 # Coneheads (1993)
|
run flix add tmdb movie 9612 # Coneheads (1993)
|
||||||
@@ -34,13 +36,16 @@ run flix add tmdb movie 71469 # The Darkest Hour (2011)
|
|||||||
run flix add tmdb movie 19901 # Daybreakers (2010)
|
run flix add tmdb movie 19901 # Daybreakers (2010)
|
||||||
run flix add tmdb movie 1097870 # Dear Santa (2024)
|
run flix add tmdb movie 1097870 # Dear Santa (2024)
|
||||||
run flix add tmdb movie 351460 # Death Note (2017)
|
run flix add tmdb movie 351460 # Death Note (2017)
|
||||||
|
run flix add tmdb movie 395990 # Death Wish (2018)
|
||||||
run flix add tmdb movie 76493 # The Dictator (2012)
|
run flix add tmdb movie 76493 # The Dictator (2012)
|
||||||
|
run flix add tmdb movie 13805 # Disaster Movie (2008)
|
||||||
run flix add tmdb movie 10398 # Double Jeopardy (1999)
|
run flix add tmdb movie 10398 # Double Jeopardy (1999)
|
||||||
run flix add tmdb movie 37735 # Easy A (2010)
|
run flix add tmdb movie 37735 # Easy A (2010)
|
||||||
run flix add tmdb movie 777443 # The Electric State (2025)
|
run flix add tmdb movie 777443 # The Electric State (2025)
|
||||||
run flix add tmdb movie 976573 # Elemental (2023)
|
run flix add tmdb movie 976573 # Elemental (2023)
|
||||||
run flix add tmdb movie 10719 # Elf (2003)
|
run flix add tmdb movie 10719 # Elf (2003)
|
||||||
run flix add tmdb movie 1022787 # Elio (2025)
|
run flix add tmdb movie 1022787 # Elio (2025)
|
||||||
|
run flix add tmdb movie 9760 # Epic Movie (2007)
|
||||||
run flix add tmdb movie 7299 # Equilibrium (2002)
|
run flix add tmdb movie 7299 # Equilibrium (2002)
|
||||||
run flix add tmdb movie 746036 # The Fall Guy (2024)
|
run flix add tmdb movie 746036 # The Fall Guy (2024)
|
||||||
run flix add tmdb movie 11619 # Flushed Away (2006)
|
run flix add tmdb movie 11619 # Flushed Away (2006)
|
||||||
@@ -57,20 +62,25 @@ run flix add tmdb movie 756397 # Hitpig! (2024)
|
|||||||
run flix add tmdb movie 228161 # Home (2015)
|
run flix add tmdb movie 228161 # Home (2015)
|
||||||
run flix add tmdb movie 204082 # Homefront (2013)
|
run flix add tmdb movie 204082 # Homefront (2013)
|
||||||
run flix add tmdb movie 1327819 # Hoppers (2026)
|
run flix add tmdb movie 1327819 # Hoppers (2026)
|
||||||
|
run flix add tmdb movie 24115 # How to Eat Fried Worms (2006)
|
||||||
run flix add tmdb movie 136418 # Hummingbird (2013)
|
run flix add tmdb movie 136418 # Hummingbird (2013)
|
||||||
|
run flix add tmdb movie 231474 # The Hunters (2013)
|
||||||
run flix add tmdb movie 7512 # Idiocracy (2006)
|
run flix add tmdb movie 7512 # Idiocracy (2006)
|
||||||
run flix add tmdb movie 639720 # IF (2024)
|
run flix add tmdb movie 639720 # IF (2024)
|
||||||
|
run flix add tmdb movie 566927 # Impractical Jokers: The Movie (2020)
|
||||||
run flix add tmdb movie 49530 # In Time (2011)
|
run flix add tmdb movie 49530 # In Time (2011)
|
||||||
run flix add tmdb movie 27205 # Inception (2010)
|
run flix add tmdb movie 27205 # Inception (2010)
|
||||||
run flix add tmdb movie 228967 # The Interview (2014)
|
run flix add tmdb movie 228967 # The Interview (2014)
|
||||||
run flix add tmdb movie 1079091 # It Ends with Us (2024)
|
run flix add tmdb movie 1079091 # It Ends with Us (2024)
|
||||||
run flix add tmdb movie 9654 # The Italian Job (2003)
|
run flix add tmdb movie 9654 # The Italian Job (2003)
|
||||||
|
run flix add tmdb movie 71880 # Jack and Jill (2011)
|
||||||
run flix add tmdb movie 12589 # Jimmy Neutron: Boy Genius (2001)
|
run flix add tmdb movie 12589 # Jimmy Neutron: Boy Genius (2001)
|
||||||
run flix add tmdb movie 9886 # Johnny Mnemonic (1995)
|
run flix add tmdb movie 9886 # Johnny Mnemonic (1995)
|
||||||
run flix add tmdb movie 8247 # Jumper (2008)
|
run flix add tmdb movie 8247 # Jumper (2008)
|
||||||
run flix add tmdb movie 451048 # Jungle Cruise (2021)
|
run flix add tmdb movie 451048 # Jungle Cruise (2021)
|
||||||
run flix add tmdb movie 76757 # Jupiter Ascending (2015)
|
run flix add tmdb movie 76757 # Jupiter Ascending (2015)
|
||||||
run flix add tmdb movie 49021 # Killer Elite (2011)
|
run flix add tmdb movie 49021 # Killer Elite (2011)
|
||||||
|
run flix add tmdb movie 1368881 # Ladies First (2026)
|
||||||
run flix add tmdb movie 1210646 # Lady Ballers (2023)
|
run flix add tmdb movie 1210646 # Lady Ballers (2023)
|
||||||
run flix add tmdb movie 586954 # Latte and the Magic Waterstone (2019)
|
run flix add tmdb movie 586954 # Latte and the Magic Waterstone (2019)
|
||||||
run flix add tmdb movie 726209 # Leave the World Behind (2023)
|
run flix add tmdb movie 726209 # Leave the World Behind (2023)
|
||||||
@@ -86,6 +96,7 @@ run flix add tmdb movie 240832 # Lucy (2014)
|
|||||||
run flix add tmdb movie 286217 # The Martian (2015)
|
run flix add tmdb movie 286217 # The Martian (2015)
|
||||||
run flix add tmdb movie 13908 # The Master of Disguise (2002)
|
run flix add tmdb movie 13908 # The Master of Disguise (2002)
|
||||||
run flix add tmdb movie 673593 # Mean Girls (2024)
|
run flix add tmdb movie 673593 # Mean Girls (2024)
|
||||||
|
run flix add tmdb movie 7278 # Meet the Spartans (2008)
|
||||||
run flix add tmdb movie 593643 # The Menu (2022)
|
run flix add tmdb movie 593643 # The Menu (2022)
|
||||||
run flix add tmdb movie 940551 # Migration (2023)
|
run flix add tmdb movie 940551 # Migration (2023)
|
||||||
run flix add tmdb movie 188161 # A Million Ways to Die in the West (2014)
|
run flix add tmdb movie 188161 # A Million Ways to Die in the West (2014)
|
||||||
@@ -96,7 +107,10 @@ run flix add tmdb movie 15512 # Monsters vs Aliens (2009)
|
|||||||
run flix add tmdb movie 428078 # Mortal Engines (2018)
|
run flix add tmdb movie 428078 # Mortal Engines (2018)
|
||||||
run flix add tmdb movie 787 # Mr. & Mrs. Smith (2005)
|
run flix add tmdb movie 787 # Mr. & Mrs. Smith (2005)
|
||||||
run flix add tmdb movie 9824 # Mystery Men (1999)
|
run flix add tmdb movie 9824 # Mystery Men (1999)
|
||||||
|
run flix add tmdb movie 328387 # Nerve (2016)
|
||||||
run flix add tmdb movie 242582 # Nightcrawler (2014)
|
run flix add tmdb movie 242582 # Nightcrawler (2014)
|
||||||
|
run flix add tmdb movie 77234 # The Numbers Station (2013)
|
||||||
|
run flix add tmdb movie 1339713 # Obsession (2026)
|
||||||
run flix add tmdb movie 179826 # Odd Thomas (2013)
|
run flix add tmdb movie 179826 # Odd Thomas (2013)
|
||||||
run flix add tmdb movie 51828 # One Day (2011)
|
run flix add tmdb movie 51828 # One Day (2011)
|
||||||
run flix add tmdb movie 10796 # The One (2001)
|
run flix add tmdb movie 10796 # The One (2001)
|
||||||
@@ -123,11 +137,12 @@ run flix add tmdb movie 72387 # Safe (2012)
|
|||||||
run flix add tmdb movie 27576 # Salt (2010)
|
run flix add tmdb movie 27576 # Salt (2010)
|
||||||
run flix add tmdb movie 254128 # San Andreas (2015)
|
run flix add tmdb movie 254128 # San Andreas (2015)
|
||||||
run flix add tmdb movie 223702 # Sausage Party (2016)
|
run flix add tmdb movie 223702 # Sausage Party (2016)
|
||||||
run flix add tmdb movie 27022 # The Sorcerer's Apprentice (2010)
|
run flix add tmdb movie 942199 # Simulant (2023)
|
||||||
run flix add tmdb movie 11459 # Sky High (2005)
|
run flix add tmdb movie 11459 # Sky High (2005)
|
||||||
run flix add tmdb movie 1084153 # Sneaks (2025)
|
run flix add tmdb movie 1084153 # Sneaks (2025)
|
||||||
run flix add tmdb movie 447273 # Snow White (2025)
|
run flix add tmdb movie 447273 # Snow White (2025)
|
||||||
run flix add tmdb movie 110415 # Snowpiercer (2013)
|
run flix add tmdb movie 110415 # Snowpiercer (2013)
|
||||||
|
run flix add tmdb movie 27022 # The Sorcerer's Apprentice (2010)
|
||||||
run flix add tmdb movie 592983 # Spellbound (2024)
|
run flix add tmdb movie 592983 # Spellbound (2024)
|
||||||
run flix add tmdb movie 238713 # Spy (2015)
|
run flix add tmdb movie 238713 # Spy (2015)
|
||||||
run flix add tmdb movie 53565 # Steamboat Willie (1928)
|
run flix add tmdb movie 53565 # Steamboat Willie (1928)
|
||||||
@@ -154,6 +169,7 @@ run flix add \
|
|||||||
tmdb movie 10681 # WALL·E (2008)
|
tmdb movie 10681 # WALL·E (2008)
|
||||||
run flix add tmdb movie 8909 # Wanted (2008)
|
run flix add tmdb movie 8909 # Wanted (2008)
|
||||||
run flix add tmdb movie 10431 # War (2007)
|
run flix add tmdb movie 10431 # War (2007)
|
||||||
|
run flix add tmdb movie 138832 # We're the Millers (2013)
|
||||||
run flix add tmdb movie 894205 # Werewolf by Night (2022)
|
run flix add tmdb movie 894205 # Werewolf by Night (2022)
|
||||||
run flix add tmdb movie 957258 # What Is a Woman? (2022)
|
run flix add tmdb movie 957258 # What Is a Woman? (2022)
|
||||||
run flix add tmdb movie 265208 # Wild Card (2015)
|
run flix add tmdb movie 265208 # Wild Card (2015)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Individual Shows
|
# Individual Shows
|
||||||
|
run flix add tmdb show 15260 # Adventure Time (2010)
|
||||||
|
run flix add tmdb show 110316 # Alice in Borderland (2020)
|
||||||
run flix add tmdb show 68421 # Altered Carbon (2018)
|
run flix add tmdb show 68421 # Altered Carbon (2018)
|
||||||
run flix add tmdb show 62110 # Assassination Classroom (2015)
|
run flix add tmdb show 62110 # Assassination Classroom (2015)
|
||||||
run flix add tmdb show 1429 # Attack on Titan (2013)
|
run flix add tmdb show 1429 # Attack on Titan (2013)
|
||||||
|
run flix add tmdb show 82452 # Avatar: The Last Airbender (2024)
|
||||||
run flix add tmdb show 42009 # Black Mirror (2011)
|
run flix add tmdb show 42009 # Black Mirror (2011)
|
||||||
run flix add tmdb show 1911 # Bones (2005)
|
run flix add tmdb show 1911 # Bones (2005)
|
||||||
run flix add tmdb show 48891 # Brooklyn Nine-Nine (2013)
|
run flix add tmdb show 48891 # Brooklyn Nine-Nine (2013)
|
||||||
@@ -9,24 +12,34 @@ run flix add tmdb show 3787 # Chaotic (2006)
|
|||||||
run flix add tmdb show 2557 # Class of the Titans (2006)
|
run flix add tmdb show 2557 # Class of the Titans (2006)
|
||||||
run flix add tmdb show 13916 # Death Note (2006)
|
run flix add tmdb show 13916 # Death Note (2006)
|
||||||
run flix add tmdb show 1405 # Dexter (2006)
|
run flix add tmdb show 1405 # Dexter (2006)
|
||||||
|
run flix add tmdb show 302919 # The End of an Era (2025)
|
||||||
|
run flix add tmdb show 85552 # Euphoria (2019)
|
||||||
run flix add tmdb show 1399 # Game of Thrones (2011)
|
run flix add tmdb show 1399 # Game of Thrones (2011)
|
||||||
run flix add tmdb show 40075 # Gravity Falls (2012)
|
run flix add tmdb show 40075 # Gravity Falls (2012)
|
||||||
run flix add tmdb show 1639 # Heroes (2006)
|
run flix add tmdb show 1639 # Heroes (2006)
|
||||||
run flix add tmdb show 60858 # Heroes Reborn (2015)
|
run flix add tmdb show 60858 # Heroes Reborn (2015)
|
||||||
|
run flix add tmdb show 31756 # Impact (2009)
|
||||||
|
run flix add tmdb show 59186 # Impractical Jokers (2011)
|
||||||
run flix add tmdb show 62687 # Limitless (2015)
|
run flix add tmdb show 62687 # Limitless (2015)
|
||||||
run flix add tmdb show 60846 # Log Horizon (2013)
|
run flix add tmdb show 60846 # Log Horizon (2013)
|
||||||
run flix add tmdb show 64432 # The Magicians (2015)
|
run flix add tmdb show 64432 # The Magicians (2015)
|
||||||
run flix add tmdb show 5920 # The Mentalist (2008)
|
run flix add tmdb show 5920 # The Mentalist (2008)
|
||||||
run flix add tmdb show 12786 # Murdoch Mysteries (2008)
|
run flix add tmdb show 12786 # Murdoch Mysteries (2008)
|
||||||
run flix add tmdb show 65930 # My Hero Academia (2016)
|
run flix add tmdb show 65930 # My Hero Academia (2016)
|
||||||
|
run flix add tmdb show 103540 # Percy Jackson and the Olympians (2023)
|
||||||
|
run flix add tmdb show 212055 # Plan B (2023)
|
||||||
|
run flix add tmdb show 225171 # Pluribus (2025)
|
||||||
run flix add tmdb show 2288 # Prison Break (2005)
|
run flix add tmdb show 2288 # Prison Break (2005)
|
||||||
run flix add tmdb show 95396 # Severance (2022)
|
run flix add tmdb show 95396 # Severance (2022)
|
||||||
run flix add tmdb show 60573 # Silicon Valley (2014)
|
run flix add tmdb show 60573 # Silicon Valley (2014)
|
||||||
|
run flix add tmdb show 85922 # Space Force (2020)
|
||||||
|
run flix add tmdb show 66732 # Stranger Things (2016)
|
||||||
run flix add tmdb show 37680 # Suits (2011)
|
run flix add tmdb show 37680 # Suits (2011)
|
||||||
run flix add tmdb show 45782 # Sword Art Online (2012)
|
run flix add tmdb show 45782 # Sword Art Online (2012)
|
||||||
run flix add tmdb show 63404 # Taskmaster (2015)
|
run flix add tmdb show 63404 # Taskmaster (2015)
|
||||||
run flix add tmdb show 48860 # The Tomorrow People (2013)
|
run flix add tmdb show 48860 # The Tomorrow People (2013)
|
||||||
run flix add tmdb show 46331 # Under the Dome (2013)
|
run flix add tmdb show 46331 # Under the Dome (2013)
|
||||||
|
run flix add tmdb show 18165 # The Vampire Diaries (2009)
|
||||||
run flix add tmdb show 1432 # Veronica Mars (2004)
|
run flix add tmdb show 1432 # Veronica Mars (2004)
|
||||||
run flix add tmdb show 186 # Weeds (2005)
|
run flix add tmdb show 186 # Weeds (2005)
|
||||||
run flix add tmdb show 63247 # Westworld (2016)
|
run flix add tmdb show 63247 # Westworld (2016)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix-mux"
|
name = "flix-mux"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "CLI for bulk media muxing"
|
description = "CLI for bulk media muxing"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = ["command-line-utilities"]
|
keywords = ["flix"]
|
||||||
|
categories = ["command-line-utilities", "multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -31,27 +33,10 @@ clap = { workspace = true, features = [
|
|||||||
"usage",
|
"usage",
|
||||||
] }
|
] }
|
||||||
console = { workspace = true }
|
console = { workspace = true }
|
||||||
dialoguer = { workspace = true }
|
|
||||||
indicatif = { workspace = true }
|
indicatif = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive", "std"] }
|
serde = { workspace = true, features = ["derive", "std"] }
|
||||||
serde_json = { workspace = true, features = ["alloc"] }
|
serde_json = { workspace = true, features = ["alloc"] }
|
||||||
walkdir = { workspace = true }
|
walkdir = { workspace = true }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints]
|
||||||
arithmetic_side_effects = "deny"
|
workspace = true
|
||||||
as_conversions = "deny"
|
|
||||||
checked_conversions = "deny"
|
|
||||||
default_union_representation = "deny"
|
|
||||||
expect_used = "deny"
|
|
||||||
indexing_slicing = "deny"
|
|
||||||
integer_division = "deny"
|
|
||||||
integer_division_remainder_used = "deny"
|
|
||||||
transmute_undefined_repr = "deny"
|
|
||||||
unchecked_time_subtraction = "deny"
|
|
||||||
unwrap_used = "deny"
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
arithmetic_overflow = "forbid"
|
|
||||||
missing_docs = "forbid"
|
|
||||||
unsafe_code = "forbid"
|
|
||||||
unused_doc_comments = "forbid"
|
|
||||||
|
|||||||
+30
-20
@@ -1,17 +1,18 @@
|
|||||||
use std::path::PathBuf;
|
//! Command line argument parsing.
|
||||||
|
|
||||||
use clap::Parser;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::parser::Selector;
|
use crate::parser::Selector;
|
||||||
|
|
||||||
#[derive(Parser)]
|
/// Command line argument parser struct.
|
||||||
|
#[derive(Debug, clap::Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct Cli {
|
pub(crate) struct Args {
|
||||||
/// Dry run and print commands
|
/// Dry run and print commands.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
|
||||||
/// Stream selectors
|
/// Stream selectors.
|
||||||
#[arg(
|
#[arg(
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
@@ -21,29 +22,38 @@ pub struct Cli {
|
|||||||
)]
|
)]
|
||||||
selectors: Vec<Selector>,
|
selectors: Vec<Selector>,
|
||||||
|
|
||||||
/// The path to the directory to scan
|
/// The path to the directory to scan.
|
||||||
#[arg(value_name = "DIR")]
|
#[arg(value_name = "DIR")]
|
||||||
scan_dir: PathBuf,
|
scan_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Args {
|
||||||
pub fn is_dry(&self) -> bool {
|
/// True if the user requested a dry run.
|
||||||
|
pub(crate) const fn is_dry_run(&self) -> bool {
|
||||||
self.dry_run
|
self.dry_run
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selectors(&self) -> &[Selector] {
|
/// Returns a slice of provided [Selector].
|
||||||
|
pub(crate) fn selectors(&self) -> &[Selector] {
|
||||||
&self.selectors
|
&self.selectors
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_dir_path(&self) -> PathBuf {
|
/// Get the path to scan.
|
||||||
fn expect_home_dir() -> PathBuf {
|
///
|
||||||
#[allow(clippy::expect_used)]
|
/// # Panics
|
||||||
std::env::home_dir().expect("you do not have a home directory")
|
/// If the user doesn't have a home directory.
|
||||||
}
|
pub(crate) fn scan_dir_path(&self) -> PathBuf {
|
||||||
|
self.scan_dir.strip_prefix("~/").map_or_else(
|
||||||
match self.scan_dir.strip_prefix("~/") {
|
|_| self.scan_dir.clone(),
|
||||||
Ok(path) => expect_home_dir().join(path),
|
|path| {
|
||||||
Err(_) => self.scan_dir.to_owned(),
|
#[expect(
|
||||||
}
|
clippy::expect_used,
|
||||||
|
reason = "without a home directory this program is useless"
|
||||||
|
)]
|
||||||
|
std::env::home_dir()
|
||||||
|
.expect("you do not have a home directory")
|
||||||
|
.join(path)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! flix-mux
|
//! flix-mux.
|
||||||
|
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser as _;
|
||||||
use console::style;
|
use console::style;
|
||||||
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
|
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
@@ -16,14 +16,17 @@ mod parser;
|
|||||||
mod probe;
|
mod probe;
|
||||||
mod scan;
|
mod scan;
|
||||||
|
|
||||||
|
#[cfg_attr(test, expect(clippy::missing_panics_doc, reason = "main function"))]
|
||||||
|
#[expect(clippy::print_stderr, reason = "we want to print in a binary")]
|
||||||
|
#[expect(clippy::print_stdout, reason = "we want to print in a binary")]
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Args::parse();
|
||||||
|
|
||||||
let (files, size) = scan_directory(
|
let (files, size) = scan_directory(
|
||||||
&cli.scan_dir_path(),
|
&cli.scan_dir_path(),
|
||||||
|| {
|
|| {
|
||||||
let progress = ProgressBar::new_spinner();
|
let progress = ProgressBar::new_spinner();
|
||||||
progress.set_message(format!("Scanning {:?}", &cli.scan_dir_path()));
|
progress.set_message(format!("Scanning {}", cli.scan_dir_path().display()));
|
||||||
progress.enable_steady_tick(Duration::from_millis(50));
|
progress.enable_steady_tick(Duration::from_millis(50));
|
||||||
progress
|
progress
|
||||||
},
|
},
|
||||||
@@ -31,7 +34,8 @@ fn main() {
|
|||||||
|len| {
|
|len| {
|
||||||
let progress = ProgressBar::new(u64::try_from(len).unwrap_or(0));
|
let progress = ProgressBar::new(u64::try_from(len).unwrap_or(0));
|
||||||
progress.set_style(
|
progress.set_style(
|
||||||
#[expect(clippy::expect_used)]
|
#[expect(clippy::expect_used, reason = "panic if the template is invalid")]
|
||||||
|
#[expect(clippy::literal_string_with_formatting_args, reason = "template")]
|
||||||
ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
|
ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
|
||||||
.expect("static template"),
|
.expect("static template"),
|
||||||
);
|
);
|
||||||
@@ -40,19 +44,20 @@ fn main() {
|
|||||||
|progress| progress.inc(1),
|
|progress| progress.inc(1),
|
||||||
|progress| progress.finish_and_clear(),
|
|progress| progress.finish_and_clear(),
|
||||||
|progress, msg| {
|
|progress, msg| {
|
||||||
progress.suspend(|| eprintln!("{} {}", style("[WARN]").bold().yellow(), msg))
|
progress.suspend(|| eprintln!("{} {msg}", style("[WARN]").bold().yellow()));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
println!("Found {} files ({})", files.len(), HumanBytes(size));
|
println!("Found {} files ({})", files.len(), HumanBytes(size));
|
||||||
|
|
||||||
mux_files(
|
mux_files(
|
||||||
cli.is_dry(),
|
cli.is_dry_run(),
|
||||||
&files,
|
&files,
|
||||||
cli.selectors(),
|
cli.selectors(),
|
||||||
|len| {
|
|len| {
|
||||||
let progress = ProgressBar::new(u64::try_from(len).unwrap_or(0));
|
let progress = ProgressBar::new(u64::try_from(len).unwrap_or(0));
|
||||||
progress.set_style(
|
progress.set_style(
|
||||||
#[expect(clippy::expect_used)]
|
#[expect(clippy::expect_used, reason = "panic if the template is invalid")]
|
||||||
|
#[expect(clippy::literal_string_with_formatting_args, reason = "template")]
|
||||||
ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
|
ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
|
||||||
.expect("static template"),
|
.expect("static template"),
|
||||||
);
|
);
|
||||||
@@ -61,6 +66,6 @@ fn main() {
|
|||||||
},
|
},
|
||||||
|progress| progress.inc(1),
|
|progress| progress.inc(1),
|
||||||
|progress| progress.finish_with_message("done"),
|
|progress| progress.finish_with_message("done"),
|
||||||
|progress, msg| progress.suspend(|| eprintln!("{} {}", style("[ERR]").bold().red(), msg)),
|
|progress, msg| progress.suspend(|| eprintln!("{} {msg}", style("[ERR]").bold().red())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-16
@@ -1,42 +1,61 @@
|
|||||||
|
//! Model for handling media streams.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Represents a file that was scanned.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MediaFile {
|
pub(crate) struct MediaFile {
|
||||||
|
/// The path to the file.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
/// The size of the file.
|
||||||
pub byte_size: u64,
|
pub byte_size: u64,
|
||||||
|
/// The streams in the file.
|
||||||
pub streams: Streams,
|
pub streams: Streams,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the collection of streams found in a media file.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Streams {
|
pub(crate) struct Streams {
|
||||||
|
/// The video streams.
|
||||||
pub video: Vec<VideoStream>,
|
pub video: Vec<VideoStream>,
|
||||||
|
/// The audio streams.
|
||||||
pub audio: Vec<AudioStream>,
|
pub audio: Vec<AudioStream>,
|
||||||
|
/// The subtitle streams.
|
||||||
pub subtitle: Vec<SubtitleStream>,
|
pub subtitle: Vec<SubtitleStream>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FFStream: serde::de::DeserializeOwned {
|
/// This trait helps with deserializing `ffmpeg` output.
|
||||||
|
pub(crate) trait FFStream: serde::de::DeserializeOwned {
|
||||||
|
/// The type name that `ffmpeg` uses.
|
||||||
const FF_TYPE_NAME: &str;
|
const FF_TYPE_NAME: &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents an individual video stream in a media file.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct VideoStream {
|
pub(crate) struct VideoStream {
|
||||||
|
/// The name of the video codec.
|
||||||
codec_name: String,
|
codec_name: String,
|
||||||
|
/// Additional tags on the stream.
|
||||||
tags: Option<VideoTags>,
|
tags: Option<VideoTags>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents tags on a video stream.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct VideoTags {
|
pub(crate) struct VideoTags {
|
||||||
|
/// The language of the stream.
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoStream {
|
impl VideoStream {
|
||||||
pub fn codec(&self) -> &str {
|
/// Get the video codec name.
|
||||||
|
pub(crate) fn codec(&self) -> &str {
|
||||||
&self.codec_name
|
&self.codec_name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language(&self) -> Option<&str> {
|
/// Get the stream language.
|
||||||
|
pub(crate) fn language(&self) -> Option<&str> {
|
||||||
self.tags.as_ref()?.language.as_deref()
|
self.tags.as_ref()?.language.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,23 +64,30 @@ impl FFStream for VideoStream {
|
|||||||
const FF_TYPE_NAME: &str = "v";
|
const FF_TYPE_NAME: &str = "v";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents an individual audio stream in a media file.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct AudioStream {
|
pub(crate) struct AudioStream {
|
||||||
|
/// The name of the audio codec.
|
||||||
codec_name: String,
|
codec_name: String,
|
||||||
|
/// Additional tags on the stream.
|
||||||
tags: Option<AudioTags>,
|
tags: Option<AudioTags>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents tags on an audio stream.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct AudioTags {
|
pub(crate) struct AudioTags {
|
||||||
|
/// The language of the stream.
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioStream {
|
impl AudioStream {
|
||||||
pub fn codec(&self) -> &str {
|
/// Get the audio codec name.
|
||||||
|
pub(crate) fn codec(&self) -> &str {
|
||||||
&self.codec_name
|
&self.codec_name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language(&self) -> Option<&str> {
|
/// Get the stream language.
|
||||||
|
pub(crate) fn language(&self) -> Option<&str> {
|
||||||
self.tags.as_ref()?.language.as_deref()
|
self.tags.as_ref()?.language.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,28 +96,37 @@ impl FFStream for AudioStream {
|
|||||||
const FF_TYPE_NAME: &str = "a";
|
const FF_TYPE_NAME: &str = "a";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents an individual subtitle stream in a media file.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct SubtitleStream {
|
pub(crate) struct SubtitleStream {
|
||||||
|
/// The name of the subtitle codec.
|
||||||
codec_name: String,
|
codec_name: String,
|
||||||
|
/// Additional tags on the stream.
|
||||||
tags: Option<SubtitleTags>,
|
tags: Option<SubtitleTags>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents tags on a subtitle stream.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct SubtitleTags {
|
pub(crate) struct SubtitleTags {
|
||||||
|
/// The language of the stream.
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
|
/// The title of the stream.
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubtitleStream {
|
impl SubtitleStream {
|
||||||
pub fn codec(&self) -> &str {
|
/// Get the subtitle codec name.
|
||||||
|
pub(crate) fn codec(&self) -> &str {
|
||||||
&self.codec_name
|
&self.codec_name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language(&self) -> Option<&str> {
|
/// Get the stream language.
|
||||||
|
pub(crate) fn language(&self) -> Option<&str> {
|
||||||
self.tags.as_ref()?.language.as_deref()
|
self.tags.as_ref()?.language.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(&self) -> Option<&str> {
|
/// Get the stream title.
|
||||||
|
pub(crate) fn title(&self) -> Option<&str> {
|
||||||
self.tags.as_ref()?.title.as_deref()
|
self.tags.as_ref()?.title.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-38
@@ -1,3 +1,5 @@
|
|||||||
|
//! Wrapper around `ffmpeg` to mux media files.
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
@@ -5,14 +7,19 @@ use anyhow::{Context as _, Result};
|
|||||||
use crate::model::MediaFile;
|
use crate::model::MediaFile;
|
||||||
use crate::parser::{Matcher, Selector, StreamFlag, StreamType};
|
use crate::parser::{Matcher, Selector, StreamFlag, StreamType};
|
||||||
|
|
||||||
|
/// Helper struct for tracking which output index is currently available.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct OutputIndex {
|
struct OutputIndex {
|
||||||
|
/// The next free video index.
|
||||||
video: usize,
|
video: usize,
|
||||||
|
/// The next free audio index.
|
||||||
audio: usize,
|
audio: usize,
|
||||||
|
/// The next free subtitle index.
|
||||||
subtitle: usize,
|
subtitle: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mux_files<T>(
|
/// For each input file, apply all selectors and mux the selection.
|
||||||
|
pub(crate) fn mux_files<T>(
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
files: &[MediaFile],
|
files: &[MediaFile],
|
||||||
selectors: &[Selector],
|
selectors: &[Selector],
|
||||||
@@ -24,14 +31,20 @@ pub fn mux_files<T>(
|
|||||||
let mut progress = fixed_length_start(files.len());
|
let mut progress = fixed_length_start(files.len());
|
||||||
for file in files {
|
for file in files {
|
||||||
if let Err(err) = mux(dry_run, file, selectors) {
|
if let Err(err) = mux(dry_run, file, selectors) {
|
||||||
print_fn(&progress, &format!("{:?}", err));
|
print_fn(&progress, &format!("{err:?}"));
|
||||||
}
|
}
|
||||||
fixed_length_update(&mut progress);
|
fixed_length_update(&mut progress);
|
||||||
}
|
}
|
||||||
fixed_length_end(progress);
|
fixed_length_end(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::expect_used)]
|
/// Apply all selectors and mux the file.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the call to `ffmpeg` fails.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If the system fails to allocate a temporary file.
|
||||||
fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
|
fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
|
||||||
let mut command = Command::new("ffmpeg");
|
let mut command = Command::new("ffmpeg");
|
||||||
let mut command = command.args(["-v", "error"]);
|
let mut command = command.args(["-v", "error"]);
|
||||||
@@ -40,10 +53,14 @@ fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
|
|||||||
command = command.arg(file.path.as_os_str());
|
command = command.arg(file.path.as_os_str());
|
||||||
|
|
||||||
for selector in selectors {
|
for selector in selectors {
|
||||||
command = command.args(
|
command = command.args(make_map_args(file, selector).with_context(|| {
|
||||||
make_map_args(file, selector)
|
let info = if dry_run {
|
||||||
.with_context(|| format!("Failed to mux {:?}", file.path))?,
|
format!("Streams:\n{file:#?}")
|
||||||
);
|
} else {
|
||||||
|
String::from("use -d for stream info")
|
||||||
|
};
|
||||||
|
format!("Failed to mux {}\n{info}", file.path.display())
|
||||||
|
})?);
|
||||||
}
|
}
|
||||||
|
|
||||||
command = command.args(["-c:v", "copy", "-c:a", "copy", "-c:s", "mov_text"]);
|
command = command.args(["-c:v", "copy", "-c:a", "copy", "-c:s", "mov_text"]);
|
||||||
@@ -56,25 +73,34 @@ fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
|
|||||||
let mut ouput_index = OutputIndex::default();
|
let mut ouput_index = OutputIndex::default();
|
||||||
for selector in selectors {
|
for selector in selectors {
|
||||||
command = command.args(
|
command = command.args(
|
||||||
make_metadata_args(file, selector, &mut ouput_index)
|
make_metadata_args(file, selector, &mut ouput_index).with_context(|| {
|
||||||
.with_context(|| format!("Failed to mux {:?}", file.path))?,
|
let info = if dry_run {
|
||||||
|
format!("Streams:\n{file:#?}")
|
||||||
|
} else {
|
||||||
|
String::from("use -d for stream info")
|
||||||
|
};
|
||||||
|
format!("Failed to mux {}\n{info}", file.path.display())
|
||||||
|
})?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let temp_path = file.path.with_extension("mp4");
|
let temp_path = file.path.with_extension("mp4");
|
||||||
|
#[expect(clippy::expect_used, reason = "files must have names")]
|
||||||
|
{
|
||||||
command = command.arg(temp_path.file_name().expect("file name exists"));
|
command = command.arg(temp_path.file_name().expect("file name exists"));
|
||||||
|
}
|
||||||
|
|
||||||
if dry_run {
|
if dry_run {
|
||||||
print_command(command);
|
print_command(command);
|
||||||
} else {
|
} else {
|
||||||
let output = command
|
let output = command
|
||||||
.output()
|
.output()
|
||||||
.with_context(|| format!("Failed to mux {:?}", file.path))?;
|
.with_context(|| format!("Failed to mux {}", file.path.display()))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"ffmpeg failed for {:?}:\n\n{}",
|
"ffmpeg failed for {}:\n\n{}",
|
||||||
file.path,
|
file.path.display(),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,23 +109,31 @@ fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate the `-map` argument list for `ffmpeg`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the stream selection is unsatisfyable for the file.
|
||||||
fn make_map_args(file: &MediaFile, selector: &Selector) -> Result<Vec<String>> {
|
fn make_map_args(file: &MediaFile, selector: &Selector) -> Result<Vec<String>> {
|
||||||
let source_index = 0;
|
let source_index: u32 = 0;
|
||||||
let stream_type = selector.stream_type.as_ref();
|
let stream_type = selector.stream_type.as_ref();
|
||||||
let Some(stream_index) = find_stream_index(file, selector) else {
|
let Some(stream_index) = find_stream_index(file, selector) else {
|
||||||
if selector.optional {
|
if selector.optional {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
} else {
|
|
||||||
anyhow::bail!("unsatisfied stream selection");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("unsatisfied stream selection");
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
String::from("-map"),
|
String::from("-map"),
|
||||||
format!("{}:{}:{}", source_index, stream_type, stream_index),
|
format!("{source_index}:{stream_type}:{stream_index}"),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate the `-metadata` argument list for `ffmpeg`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the stream selection is unsatisfyable for the file.
|
||||||
fn make_metadata_args(
|
fn make_metadata_args(
|
||||||
file: &MediaFile,
|
file: &MediaFile,
|
||||||
selector: &Selector,
|
selector: &Selector,
|
||||||
@@ -112,9 +146,9 @@ fn make_metadata_args(
|
|||||||
let Some(_) = find_stream_index(file, selector) else {
|
let Some(_) = find_stream_index(file, selector) else {
|
||||||
if selector.optional {
|
if selector.optional {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
} else {
|
|
||||||
anyhow::bail!("unsatisfied stream selection");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("unsatisfied stream selection");
|
||||||
};
|
};
|
||||||
|
|
||||||
let counter = match selector.stream_type {
|
let counter = match selector.stream_type {
|
||||||
@@ -126,8 +160,8 @@ fn make_metadata_args(
|
|||||||
*counter = counter.saturating_add(1);
|
*counter = counter.saturating_add(1);
|
||||||
|
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
format!("-metadata:s:{}:{}", stream_type, stream_index),
|
format!("-metadata:s:{stream_type}:{stream_index}"),
|
||||||
format!("language={}", stream_language),
|
format!("language={stream_language}"),
|
||||||
];
|
];
|
||||||
|
|
||||||
if selector.stream_type == StreamType::Subtitle {
|
if selector.stream_type == StreamType::Subtitle {
|
||||||
@@ -137,25 +171,26 @@ fn make_metadata_args(
|
|||||||
None => match stream_language {
|
None => match stream_language {
|
||||||
"eng" => "English",
|
"eng" => "English",
|
||||||
"jpn" => "Japanese",
|
"jpn" => "Japanese",
|
||||||
_ => anyhow::bail!("Unhandled subtitle language: {}", stream_language),
|
_ => anyhow::bail!("Unhandled subtitle language: {stream_language}"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
args.push(format!("-metadata:s:s:{}", stream_index));
|
args.push(format!("-metadata:s:s:{stream_index}"));
|
||||||
args.push(format!("title={}", sub_title));
|
args.push(format!("title={sub_title}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(args)
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Walk the streams of a file to match the given [Selector].
|
||||||
fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
||||||
let needs_forced = selector.flag == Some(StreamFlag::Forced);
|
let needs_forced = selector.flag == Some(StreamFlag::Forced);
|
||||||
let needs_sdh = selector.flag == Some(StreamFlag::Sdh);
|
let needs_sdh = selector.flag == Some(StreamFlag::Sdh);
|
||||||
|
|
||||||
match selector.stream_type {
|
match selector.stream_type {
|
||||||
StreamType::Video => match selector.matcher {
|
StreamType::Video => match &selector.matcher {
|
||||||
Matcher::Index(index) => (file.streams.video.len() > index).then_some(index),
|
Matcher::Index(index) => (file.streams.video.len() > *index).then_some(*index),
|
||||||
Matcher::Language(ref language) => file
|
Matcher::Language(language) => file
|
||||||
.streams
|
.streams
|
||||||
.video
|
.video
|
||||||
.iter()
|
.iter()
|
||||||
@@ -163,7 +198,7 @@ fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
|||||||
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.next(),
|
.next(),
|
||||||
Matcher::Codec(ref codec) => file
|
Matcher::Codec(codec) => file
|
||||||
.streams
|
.streams
|
||||||
.video
|
.video
|
||||||
.iter()
|
.iter()
|
||||||
@@ -172,9 +207,9 @@ fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
|||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.next(),
|
.next(),
|
||||||
},
|
},
|
||||||
StreamType::Audio => match selector.matcher {
|
StreamType::Audio => match &selector.matcher {
|
||||||
Matcher::Index(index) => (file.streams.audio.len() > index).then_some(index),
|
Matcher::Index(index) => (file.streams.audio.len() > *index).then_some(*index),
|
||||||
Matcher::Language(ref language) => file
|
Matcher::Language(language) => file
|
||||||
.streams
|
.streams
|
||||||
.audio
|
.audio
|
||||||
.iter()
|
.iter()
|
||||||
@@ -182,7 +217,7 @@ fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
|||||||
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.next(),
|
.next(),
|
||||||
Matcher::Codec(ref codec) => file
|
Matcher::Codec(codec) => file
|
||||||
.streams
|
.streams
|
||||||
.audio
|
.audio
|
||||||
.iter()
|
.iter()
|
||||||
@@ -191,9 +226,9 @@ fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
|||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.next(),
|
.next(),
|
||||||
},
|
},
|
||||||
StreamType::Subtitle => match selector.matcher {
|
StreamType::Subtitle => match &selector.matcher {
|
||||||
Matcher::Index(index) => (file.streams.subtitle.len() > index).then_some(index),
|
Matcher::Index(index) => (file.streams.subtitle.len() > *index).then_some(*index),
|
||||||
Matcher::Language(ref language) => {
|
Matcher::Language(language) => {
|
||||||
file.streams
|
file.streams
|
||||||
.subtitle
|
.subtitle
|
||||||
.iter()
|
.iter()
|
||||||
@@ -214,7 +249,7 @@ fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
|||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.next()
|
.next()
|
||||||
}
|
}
|
||||||
Matcher::Codec(ref codec) => file
|
Matcher::Codec(codec) => file
|
||||||
.streams
|
.streams
|
||||||
.subtitle
|
.subtitle
|
||||||
.iter()
|
.iter()
|
||||||
@@ -226,6 +261,8 @@ fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print out the command to stdout.
|
||||||
|
#[expect(clippy::print_stdout, reason = "the purpose is to print")]
|
||||||
fn print_command(cmd: &Command) {
|
fn print_command(cmd: &Command) {
|
||||||
let program = cmd.get_program().to_string_lossy();
|
let program = cmd.get_program().to_string_lossy();
|
||||||
|
|
||||||
@@ -235,15 +272,16 @@ fn print_command(cmd: &Command) {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
println!("{} {}", program, args);
|
println!("{program} {args}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escape a string to be used by a shell.
|
||||||
fn shell_escape(s: &str) -> String {
|
fn shell_escape(s: &str) -> String {
|
||||||
if s.chars()
|
if s.chars()
|
||||||
.all(|c| c.is_ascii_alphanumeric() || "-_./".contains(c))
|
.all(|c| c.is_ascii_alphanumeric() || "-_./".contains(c))
|
||||||
{
|
{
|
||||||
s.to_string()
|
s.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("{:?}", s)
|
format!("{s:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
|
//! Parsing types for `ffmpeg` output.
|
||||||
|
|
||||||
use core::error::Error;
|
use core::error::Error;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
/// The type of a stream.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum StreamType {
|
pub(crate) enum StreamType {
|
||||||
|
/// A video stream.
|
||||||
Video,
|
Video,
|
||||||
|
/// An audio stream.
|
||||||
Audio,
|
Audio,
|
||||||
|
/// A subtitle stream.
|
||||||
Subtitle,
|
Subtitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for StreamType {
|
impl AsRef<str> for StreamType {
|
||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
StreamType::Video => "v",
|
Self::Video => "v",
|
||||||
StreamType::Audio => "a",
|
Self::Audio => "a",
|
||||||
StreamType::Subtitle => "s",
|
Self::Subtitle => "s",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,18 +30,22 @@ impl FromStr for StreamType {
|
|||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"v" => Ok(StreamType::Video),
|
"v" => Ok(Self::Video),
|
||||||
"a" => Ok(StreamType::Audio),
|
"a" => Ok(Self::Audio),
|
||||||
"s" => Ok(StreamType::Subtitle),
|
"s" => Ok(Self::Subtitle),
|
||||||
other => Err(ParseSelectorError::InvalidStreamType(other.to_string())),
|
other => Err(ParseSelectorError::InvalidStreamType(other.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A property of a stream to match on.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Matcher {
|
pub(crate) enum Matcher {
|
||||||
|
/// Match a specific stream index.
|
||||||
Index(usize),
|
Index(usize),
|
||||||
|
/// Match the first stream of the given language.
|
||||||
Language(String),
|
Language(String),
|
||||||
|
/// Match the first stream of the given codec.
|
||||||
Codec(String),
|
Codec(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,19 +54,22 @@ impl FromStr for Matcher {
|
|||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if let Ok(idx) = s.parse::<usize>() {
|
if let Ok(idx) = s.parse::<usize>() {
|
||||||
return Ok(Matcher::Index(idx));
|
return Ok(Self::Index(idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.len() == 3 {
|
if s.len() == 3 {
|
||||||
return Ok(Matcher::Language(s.to_ascii_lowercase()));
|
return Ok(Self::Language(s.to_ascii_lowercase()));
|
||||||
}
|
}
|
||||||
Ok(Matcher::Codec(s.to_ascii_lowercase()))
|
Ok(Self::Codec(s.to_ascii_lowercase()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flags applied to streams.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum StreamFlag {
|
pub(crate) enum StreamFlag {
|
||||||
|
/// Forced flag.
|
||||||
Forced,
|
Forced,
|
||||||
|
/// SDH flag.
|
||||||
Sdh,
|
Sdh,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,24 +78,31 @@ impl FromStr for StreamFlag {
|
|||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_ascii_lowercase().as_str() {
|
match s.to_ascii_lowercase().as_str() {
|
||||||
"forced" => Ok(StreamFlag::Forced),
|
"forced" => Ok(Self::Forced),
|
||||||
"sdh" => Ok(StreamFlag::Sdh),
|
"sdh" => Ok(Self::Sdh),
|
||||||
other => Err(ParseSelectorError::InvalidFlag(other.to_string())),
|
other => Err(ParseSelectorError::InvalidFlag(other.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A complete selector which has everything it needs to find a specific stream.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Selector {
|
pub(crate) struct Selector {
|
||||||
|
/// The type of the stream to select.
|
||||||
pub stream_type: StreamType,
|
pub stream_type: StreamType,
|
||||||
|
/// The matcher to use when selecting.
|
||||||
pub matcher: Matcher,
|
pub matcher: Matcher,
|
||||||
|
/// A desired flag.
|
||||||
pub flag: Option<StreamFlag>,
|
pub flag: Option<StreamFlag>,
|
||||||
|
/// Whether or not to bail if the stream cannot be found.
|
||||||
pub optional: bool,
|
pub optional: bool,
|
||||||
|
/// The output language of the stream.
|
||||||
pub out_lang: String,
|
pub out_lang: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selector {
|
impl Selector {
|
||||||
pub fn language(&self) -> &str {
|
/// Get the target language of the selector.
|
||||||
|
pub(crate) fn language(&self) -> &str {
|
||||||
&self.out_lang
|
&self.out_lang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +145,7 @@ impl FromStr for Selector {
|
|||||||
(true, _) => return Err(ParseSelectorError::UnspecifiedLanguage),
|
(true, _) => return Err(ParseSelectorError::UnspecifiedLanguage),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Selector {
|
Ok(Self {
|
||||||
stream_type,
|
stream_type,
|
||||||
matcher,
|
matcher,
|
||||||
flag,
|
flag,
|
||||||
@@ -135,15 +155,22 @@ impl FromStr for Selector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Errors that can arise when parsing a selector string.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ParseSelectorError {
|
pub(crate) enum ParseSelectorError {
|
||||||
|
/// The selector string is empty.
|
||||||
Empty,
|
Empty,
|
||||||
|
/// The selector string has an invalid format.
|
||||||
InvalidFormat,
|
InvalidFormat,
|
||||||
|
/// The requested stream type in invalid.
|
||||||
InvalidStreamType(String),
|
InvalidStreamType(String),
|
||||||
|
/// The requested flag is invalid.
|
||||||
InvalidFlag(String),
|
InvalidFlag(String),
|
||||||
|
/// The target language is unspecified.
|
||||||
UnspecifiedLanguage,
|
UnspecifiedLanguage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl Error for ParseSelectorError {}
|
impl Error for ParseSelectorError {}
|
||||||
|
|
||||||
impl fmt::Display for ParseSelectorError {
|
impl fmt::Display for ParseSelectorError {
|
||||||
@@ -151,8 +178,8 @@ impl fmt::Display for ParseSelectorError {
|
|||||||
match self {
|
match self {
|
||||||
Self::Empty => write!(f, "selector was empty"),
|
Self::Empty => write!(f, "selector was empty"),
|
||||||
Self::InvalidFormat => write!(f, "invalid selector format"),
|
Self::InvalidFormat => write!(f, "invalid selector format"),
|
||||||
Self::InvalidStreamType(s) => write!(f, "invalid stream type '{}'", s),
|
Self::InvalidStreamType(s) => write!(f, "invalid stream type '{s}'"),
|
||||||
Self::InvalidFlag(s) => write!(f, "invalid stream flag '{}'", s),
|
Self::InvalidFlag(s) => write!(f, "invalid stream flag '{s}'"),
|
||||||
Self::UnspecifiedLanguage => write!(f, "unspecified output language"),
|
Self::UnspecifiedLanguage => write!(f, "unspecified output language"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::os::unix::fs::MetadataExt;
|
//! Helpers for using `ffprobe` to gather stream information from media files.
|
||||||
|
|
||||||
|
use std::os::unix::fs::MetadataExt as _;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
@@ -7,11 +9,20 @@ use walkdir::DirEntry;
|
|||||||
|
|
||||||
use crate::model::{FFStream, MediaFile, Streams};
|
use crate::model::{FFStream, MediaFile, Streams};
|
||||||
|
|
||||||
|
/// The top-level containing type for `ffprobe` output.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct FFProbeOutput<S> {
|
struct FFProbeOutput<S> {
|
||||||
|
/// The list of returned streams.
|
||||||
streams: Vec<S>,
|
streams: Vec<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls `ffprobe` on a [`DirEntry`] to gather one type of stream.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Forwards any IO or parsing errors.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If the file path is not utf8.
|
||||||
fn probe_file_streams<S: FFStream>(entry: &DirEntry) -> Result<Vec<S>> {
|
fn probe_file_streams<S: FFStream>(entry: &DirEntry) -> Result<Vec<S>> {
|
||||||
let output = Command::new("ffprobe")
|
let output = Command::new("ffprobe")
|
||||||
.args([
|
.args([
|
||||||
@@ -22,16 +33,16 @@ fn probe_file_streams<S: FFStream>(entry: &DirEntry) -> Result<Vec<S>> {
|
|||||||
"-show_streams",
|
"-show_streams",
|
||||||
"-select_streams",
|
"-select_streams",
|
||||||
S::FF_TYPE_NAME,
|
S::FF_TYPE_NAME,
|
||||||
#[expect(clippy::expect_used)]
|
#[expect(clippy::expect_used, reason = "panic if path is not utf8")]
|
||||||
entry.path().to_str().expect("path should be utf8"),
|
entry.path().to_str().expect("path should be utf8"),
|
||||||
])
|
])
|
||||||
.output()
|
.output()
|
||||||
.with_context(|| format!("Failed to run ffprobe on {:?}", entry.path()))?;
|
.with_context(|| format!("Failed to run ffprobe on {}", entry.path().display()))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"ffprobe failed for {:?}:\n\n{}",
|
"ffprobe failed for {}:\n\n{}",
|
||||||
entry.path(),
|
entry.path().display(),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,7 +51,11 @@ fn probe_file_streams<S: FFStream>(entry: &DirEntry) -> Result<Vec<S>> {
|
|||||||
Ok(parsed.streams)
|
Ok(parsed.streams)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn probe_file(entry: &DirEntry) -> Result<MediaFile> {
|
/// Calls `ffprobe` on a [`DirEntry`] to detect all streams.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Forwards any IO or parsing errors.
|
||||||
|
pub(crate) fn probe_file(entry: &DirEntry) -> Result<MediaFile> {
|
||||||
Ok(MediaFile {
|
Ok(MediaFile {
|
||||||
path: entry.path().to_path_buf(),
|
path: entry.path().to_path_buf(),
|
||||||
byte_size: entry.metadata()?.size(),
|
byte_size: entry.metadata()?.size(),
|
||||||
|
|||||||
+12
-15
@@ -1,3 +1,5 @@
|
|||||||
|
//! Helpers for filesystem scanning.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
@@ -5,7 +7,8 @@ use walkdir::WalkDir;
|
|||||||
use crate::model::MediaFile;
|
use crate::model::MediaFile;
|
||||||
use crate::probe::probe_file;
|
use crate::probe::probe_file;
|
||||||
|
|
||||||
pub fn scan_directory<T>(
|
/// Recursively scan a directory.
|
||||||
|
pub(crate) fn scan_directory<T>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
unknown_length_start: impl FnOnce() -> T,
|
unknown_length_start: impl FnOnce() -> T,
|
||||||
unknown_length_end: impl FnOnce(T),
|
unknown_length_end: impl FnOnce(T),
|
||||||
@@ -18,32 +21,26 @@ pub fn scan_directory<T>(
|
|||||||
let files: Vec<_> = WalkDir::new(path)
|
let files: Vec<_> = WalkDir::new(path)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.filter(|e| e.file_type().is_file())
|
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
let is_mkv = e
|
#[expect(clippy::filetype_is_file, reason = "we only want regular files")]
|
||||||
.path()
|
e.file_type().is_file()
|
||||||
.extension()
|
})
|
||||||
.unwrap_or_default()
|
.filter(|e| {
|
||||||
.eq_ignore_ascii_case("mkv");
|
let ext = e.path().extension().unwrap_or_default();
|
||||||
let is_mp4 = e
|
ext.eq_ignore_ascii_case("mkv") || ext.eq_ignore_ascii_case("mp4")
|
||||||
.path()
|
|
||||||
.extension()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.eq_ignore_ascii_case("mp4");
|
|
||||||
is_mkv || is_mp4
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
unknown_length_end(spinner);
|
unknown_length_end(spinner);
|
||||||
|
|
||||||
let mut progress = fixed_length_start(files.len());
|
let mut progress = fixed_length_start(files.len());
|
||||||
let mut total_byte_size = 0u64;
|
let mut total_byte_size = 0_u64;
|
||||||
let files: Vec<_> = files
|
let files: Vec<_> = files
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
let file = match probe_file(entry) {
|
let file = match probe_file(entry) {
|
||||||
Ok(file) => Some(file),
|
Ok(file) => Some(file),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
print_fn(&progress, &format!("{:?}", err));
|
print_fn(&progress, &format!("{err:?}"));
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-24
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix-cli"
|
name = "flix-cli"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "CLI for interacting with a flix database"
|
description = "CLI for interacting with a flix database"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = ["command-line-utilities"]
|
keywords = ["flix"]
|
||||||
|
categories = ["command-line-utilities", "multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -32,7 +34,6 @@ clap = { workspace = true, features = [
|
|||||||
"usage",
|
"usage",
|
||||||
] }
|
] }
|
||||||
flix = { workspace = true, features = ["tmdb"] }
|
flix = { workspace = true, features = ["tmdb"] }
|
||||||
futures = { workspace = true }
|
|
||||||
sea-orm = { workspace = true, features = ["debug-print", "runtime-tokio"] }
|
sea-orm = { workspace = true, features = ["debug-print", "runtime-tokio"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
|
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
|
||||||
@@ -40,21 +41,5 @@ toml = { workspace = true, features = ["parse", "serde"] }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints]
|
||||||
arithmetic_side_effects = "deny"
|
workspace = true
|
||||||
as_conversions = "deny"
|
|
||||||
checked_conversions = "deny"
|
|
||||||
default_union_representation = "deny"
|
|
||||||
expect_used = "deny"
|
|
||||||
indexing_slicing = "deny"
|
|
||||||
integer_division = "deny"
|
|
||||||
integer_division_remainder_used = "deny"
|
|
||||||
transmute_undefined_repr = "deny"
|
|
||||||
unchecked_time_subtraction = "deny"
|
|
||||||
unwrap_used = "deny"
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
arithmetic_overflow = "forbid"
|
|
||||||
missing_docs = "forbid"
|
|
||||||
unsafe_code = "forbid"
|
|
||||||
unused_doc_comments = "forbid"
|
|
||||||
|
|||||||
@@ -1,29 +1,40 @@
|
|||||||
|
//! Command line argument parsing for the `flix` subcommand.
|
||||||
|
|
||||||
use flix::model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix::model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
/// Subcommand for adding `flix` media.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum AddCommand {
|
pub(crate) enum AddCommand {
|
||||||
/// Add a flix collection
|
/// Add a flix collection.
|
||||||
Collection {
|
Collection {
|
||||||
|
/// The collection's title.
|
||||||
#[arg(value_name = "TITLE")]
|
#[arg(value_name = "TITLE")]
|
||||||
title: String,
|
title: String,
|
||||||
|
/// The collection's overview.
|
||||||
#[arg(value_name = "OVERVIEW")]
|
#[arg(value_name = "OVERVIEW")]
|
||||||
overview: String,
|
overview: String,
|
||||||
},
|
},
|
||||||
/// Add a flix episode
|
/// Add a flix episode.
|
||||||
Episode {
|
Episode {
|
||||||
|
/// The episode's show's web slug.
|
||||||
#[arg(value_name = "SHOW_WEB_SLUG")]
|
#[arg(value_name = "SHOW_WEB_SLUG")]
|
||||||
show_slug: String,
|
show_web_slug: String,
|
||||||
|
/// The episode's season number.
|
||||||
#[arg(value_name = "NUMBER")]
|
#[arg(value_name = "NUMBER")]
|
||||||
season_number: SeasonNumber,
|
season_number: SeasonNumber,
|
||||||
|
/// The episode's number.
|
||||||
#[arg(value_name = "NUMBER")]
|
#[arg(value_name = "NUMBER")]
|
||||||
episode_number: EpisodeNumber,
|
episode_number: EpisodeNumber,
|
||||||
|
/// The episode's title.
|
||||||
#[arg(value_name = "TITLE")]
|
#[arg(value_name = "TITLE")]
|
||||||
title: String,
|
title: String,
|
||||||
|
/// The episode's overview.
|
||||||
#[arg(value_name = "OVERVIEW")]
|
#[arg(value_name = "OVERVIEW")]
|
||||||
overview: String,
|
overview: String,
|
||||||
|
/// The episode's air date.
|
||||||
#[arg(value_name = "DATE")]
|
#[arg(value_name = "DATE")]
|
||||||
air_date: NaiveDate,
|
air_date: NaiveDate,
|
||||||
},
|
},
|
||||||
|
|||||||
+68
-52
@@ -1,15 +1,18 @@
|
|||||||
|
//! Command line argument parsing.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
pub mod flix;
|
pub(crate) mod flix;
|
||||||
pub mod tmdb;
|
pub(crate) mod tmdb;
|
||||||
|
|
||||||
|
/// Command line argument parser struct.
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct Cli {
|
pub(crate) struct Cli {
|
||||||
/// Use a custom config file
|
/// Use a custom config file.
|
||||||
#[arg(
|
#[arg(
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
@@ -17,41 +20,53 @@ pub struct Cli {
|
|||||||
default_value = "~/.config/flix/config.toml"
|
default_value = "~/.config/flix/config.toml"
|
||||||
)]
|
)]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
|
/// Use a custom cache file.
|
||||||
/// Use a custom cache file
|
|
||||||
#[arg(short = 'C', long, value_name = "FILE", default_value = "./flix.redb")]
|
#[arg(short = 'C', long, value_name = "FILE", default_value = "./flix.redb")]
|
||||||
cache: PathBuf,
|
cache: PathBuf,
|
||||||
|
/// Use a custom database file.
|
||||||
/// Use a custom database file
|
|
||||||
#[arg(short, long, value_name = "FILE", default_value = "./flix.db")]
|
#[arg(short, long, value_name = "FILE", default_value = "./flix.db")]
|
||||||
database: PathBuf,
|
database: PathBuf,
|
||||||
|
/// Enable tracing.
|
||||||
/// Enable tracing
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub trace: bool,
|
trace: bool,
|
||||||
|
/// Subcommand to execute.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
pub fn config_path(&self) -> PathBuf {
|
/// Get the config file path.
|
||||||
fn expect_home_dir() -> PathBuf {
|
///
|
||||||
#[allow(clippy::expect_used)]
|
/// # Panics
|
||||||
std::env::home_dir().expect("you do not have a home directory")
|
/// If the user doesn't have a home directory.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn config_path(&self) -> PathBuf {
|
||||||
|
self.config.strip_prefix("~/").map_or_else(
|
||||||
|
|_| self.config.clone(),
|
||||||
|
|path| {
|
||||||
|
#[expect(
|
||||||
|
clippy::expect_used,
|
||||||
|
reason = "without a home directory this program is useless"
|
||||||
|
)]
|
||||||
|
std::env::home_dir()
|
||||||
|
.expect("you do not have a home directory")
|
||||||
|
.join(path)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.config.strip_prefix("~/") {
|
/// Get the path to the cache file.
|
||||||
Ok(path) => expect_home_dir().join(path),
|
#[inline]
|
||||||
Err(_) => self.config.to_owned(),
|
pub(crate) fn cache_path(&self) -> &Path {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_path(&self) -> &Path {
|
|
||||||
&self.cache
|
&self.cache
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn database_path(&self) -> Result<String> {
|
/// Get the path to the destination database.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the database path is not utf8.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn database_path(&self) -> Result<String> {
|
||||||
self.database
|
self.database
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -59,77 +74,78 @@ impl Cli {
|
|||||||
.ok_or_else(|| anyhow!(".as_os_str().to_str()"))
|
.ok_or_else(|| anyhow!(".as_os_str().to_str()"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn command(self) -> Command {
|
/// Get whether or not the trace flag was enabled.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) const fn trace(&self) -> bool {
|
||||||
|
self.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the subcommand to run.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn get_command(self) -> Command {
|
||||||
self.command
|
self.command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional overrides for media information.
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct AddOverrides {
|
pub(crate) struct AddOverrides {
|
||||||
|
/// Override the displayed title.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
/// Override the title used to sort.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub sort_title: Option<String>,
|
pub sort_title: Option<String>,
|
||||||
|
/// Override the filesystem slug.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub fs_slug: Option<String>,
|
pub fs_slug: Option<String>,
|
||||||
|
/// Overrride the web slug.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub web_slug: Option<String>,
|
pub web_slug: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Top level cli commands.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Command {
|
pub(crate) enum Command {
|
||||||
/// Initialize a new database
|
/// Initialize a new database.
|
||||||
Init,
|
Init,
|
||||||
/// Add new items to the database
|
/// Add new items to the database.
|
||||||
Add {
|
Add {
|
||||||
|
/// Overrides.
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
overrides: AddOverrides,
|
overrides: AddOverrides,
|
||||||
|
/// Command.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: AddCommand,
|
command: AddCommand,
|
||||||
},
|
},
|
||||||
/// Update an existing item in the database
|
|
||||||
Update {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: UpdateCommand,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper for `add` around different backends.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum AddCommand {
|
pub(crate) enum AddCommand {
|
||||||
/// Use the flix backend
|
/// Use the flix backend.
|
||||||
Flix {
|
Flix {
|
||||||
|
/// Command backend.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: flix::AddCommand,
|
command: flix::AddCommand,
|
||||||
},
|
},
|
||||||
/// Use the TMDB backend
|
/// Use the TMDB backend.
|
||||||
Tmdb {
|
Tmdb {
|
||||||
|
/// Command backend.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: tmdb::Command,
|
command: tmdb::Command,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<flix::AddCommand> for AddCommand {
|
impl From<flix::AddCommand> for AddCommand {
|
||||||
|
#[inline]
|
||||||
fn from(value: flix::AddCommand) -> Self {
|
fn from(value: flix::AddCommand) -> Self {
|
||||||
Self::Flix { command: value }
|
Self::Flix { command: value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<tmdb::Command> for AddCommand {
|
impl From<tmdb::Command> for AddCommand {
|
||||||
fn from(value: tmdb::Command) -> Self {
|
#[inline]
|
||||||
Self::Tmdb { command: value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum UpdateCommand {
|
|
||||||
/// Use the TMDB backend
|
|
||||||
Tmdb {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: tmdb::Command,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<tmdb::Command> for UpdateCommand {
|
|
||||||
fn from(value: tmdb::Command) -> Self {
|
fn from(value: tmdb::Command) -> Self {
|
||||||
Self::Tmdb { command: value }
|
Self::Tmdb { command: value }
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-12
@@ -1,41 +1,53 @@
|
|||||||
|
//! Command line argument parsing for the `tmdb` subcommand.
|
||||||
|
|
||||||
use flix::model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix::model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
use flix::tmdb::model::id::RawId;
|
use flix::tmdb::model::id::TmdbRepr;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
/// Subcommand for adding `tmdb` media.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Command {
|
pub(crate) enum Command {
|
||||||
/// Process a TMDB collection
|
/// Process a TMDB collection.
|
||||||
Collection {
|
Collection {
|
||||||
|
/// The collection's ID.
|
||||||
#[arg(value_name = "TMDB_ID")]
|
#[arg(value_name = "TMDB_ID")]
|
||||||
id: RawId,
|
id: TmdbRepr,
|
||||||
},
|
},
|
||||||
/// Process a TMDB movie
|
/// Process a TMDB movie.
|
||||||
Movie {
|
Movie {
|
||||||
|
/// The movie's ID.
|
||||||
#[arg(value_name = "TMDB_ID")]
|
#[arg(value_name = "TMDB_ID")]
|
||||||
id: RawId,
|
id: TmdbRepr,
|
||||||
},
|
},
|
||||||
/// Process a TMDB show
|
/// Process a TMDB show.
|
||||||
Show {
|
Show {
|
||||||
|
/// The show's ID.
|
||||||
#[arg(value_name = "TMDB_ID")]
|
#[arg(value_name = "TMDB_ID")]
|
||||||
id: RawId,
|
id: TmdbRepr,
|
||||||
},
|
},
|
||||||
/// Process a TMDB season
|
/// Process a TMDB season.
|
||||||
Season {
|
Season {
|
||||||
|
/// The season's show's ID.
|
||||||
#[arg(value_name = "TMDB_ID")]
|
#[arg(value_name = "TMDB_ID")]
|
||||||
id: RawId,
|
id: TmdbRepr,
|
||||||
|
/// The season's number.
|
||||||
#[arg(value_name = "SEASON_NUM")]
|
#[arg(value_name = "SEASON_NUM")]
|
||||||
season: SeasonNumber,
|
season: SeasonNumber,
|
||||||
},
|
},
|
||||||
/// Process a TMDB episode
|
/// Process a TMDB episode.
|
||||||
#[command(trailing_var_arg = true)]
|
#[command(trailing_var_arg = true)]
|
||||||
Episode {
|
Episode {
|
||||||
|
/// The episode's show's ID.
|
||||||
#[arg(value_name = "TMDB_ID")]
|
#[arg(value_name = "TMDB_ID")]
|
||||||
id: RawId,
|
id: TmdbRepr,
|
||||||
|
/// The episode's season's number.
|
||||||
#[arg(value_name = "SEASON_NUM")]
|
#[arg(value_name = "SEASON_NUM")]
|
||||||
season: SeasonNumber,
|
season: SeasonNumber,
|
||||||
|
/// The episode's number.
|
||||||
#[arg(value_name = "EPISODE_NUM")]
|
#[arg(value_name = "EPISODE_NUM")]
|
||||||
episode: EpisodeNumber,
|
episode: EpisodeNumber,
|
||||||
|
/// Additional episode numbers for merged media files.
|
||||||
#[arg(value_name = "...")]
|
#[arg(value_name = "...")]
|
||||||
episodes: Vec<EpisodeNumber>,
|
episodes: Vec<EpisodeNumber>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
|
//! CLI configuration.
|
||||||
|
|
||||||
|
/// Top level config struct.
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct Config {
|
pub(crate) struct Config {
|
||||||
|
/// The TMDB config.
|
||||||
tmdb: TmdbConfig,
|
tmdb: TmdbConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TMDB config struct.
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct TmdbConfig {
|
pub(crate) struct TmdbConfig {
|
||||||
|
/// The bearer token to use for API requests.
|
||||||
bearer_token: String,
|
bearer_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn tmdb(&self) -> &TmdbConfig {
|
/// Get the TMDB config.
|
||||||
|
pub(crate) const fn tmdb(&self) -> &TmdbConfig {
|
||||||
&self.tmdb
|
&self.tmdb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TmdbConfig {
|
impl TmdbConfig {
|
||||||
pub fn bearer_token(&self) -> &str {
|
/// Get the bearer token.
|
||||||
|
pub(crate) fn bearer_token(&self) -> &str {
|
||||||
&self.bearer_token
|
&self.bearer_token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-6
@@ -1,9 +1,17 @@
|
|||||||
|
//! Databse helpers.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use flix::db::connection::Connection;
|
use flix::db::connection::Connection;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use sea_orm::{ConnectOptions, Database};
|
use sea_orm::{ConnectOptions, Database};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
/// Connect to a database using a connection string.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the database cannot be opened.
|
||||||
async fn connect(string: String) -> Result<Connection> {
|
async fn connect(string: String) -> Result<Connection> {
|
||||||
Connection::try_from(
|
Connection::try_from(
|
||||||
Database::connect(ConnectOptions::new(string))
|
Database::connect(ConnectOptions::new(string))
|
||||||
@@ -14,14 +22,22 @@ async fn connect(string: String) -> Result<Connection> {
|
|||||||
.context("Connection::try_from")
|
.context("Connection::try_from")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open(database_path: String) -> Result<Connection> {
|
/// Helper for opening an existing database at the given path.
|
||||||
connect(format!("sqlite:{database_path}?mode=rw")).await
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the database cannot be opened.
|
||||||
|
pub(crate) async fn open(path: &Path) -> Result<Connection> {
|
||||||
|
connect(format!("sqlite:{}?mode=rw", path.display())).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_new(database_path: String) -> Result<Connection> {
|
/// Helper for creating then opening a database at the given path.
|
||||||
if fs::try_exists(&database_path).await? {
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If the database already exists or cannot be openend after creation.
|
||||||
|
pub(crate) async fn open_new(path: &Path) -> Result<Connection> {
|
||||||
|
if fs::try_exists(path).await? {
|
||||||
bail!("database already exists");
|
bail!("database already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(format!("sqlite:{database_path}?mode=rwc")).await
|
connect(format!("sqlite:{}?mode=rwc", path.display())).await
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-27
@@ -1,15 +1,19 @@
|
|||||||
//! flix-cli
|
//! flix-cli.
|
||||||
|
|
||||||
use std::rc::Rc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
|
||||||
use flix::tmdb::{self, CachePolicy, Client, RedbCache};
|
use flix::tmdb::{self, CachePolicy, Client, RedbCache};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use clap::Parser;
|
use clap::Parser as _;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
use cli::{AddCommand, Cli, Command, UpdateCommand};
|
use cli::{AddCommand, Cli, Command};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
@@ -20,47 +24,57 @@ mod db;
|
|||||||
mod run;
|
mod run;
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
#[cfg_attr(test, expect(clippy::missing_errors_doc, reason = "main function"))]
|
||||||
|
#[cfg_attr(test, expect(clippy::missing_panics_doc, reason = "main function "))]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = fs::read_to_string(cli.config_path())
|
let config = fs::read_to_string(cli.config_path())
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("could not read config: {:?}", cli.config_path()))?;
|
.with_context(|| format!("could not read config: {}", cli.config_path().display()))?;
|
||||||
let config: Config = toml::from_str(config.as_str())
|
let config: Config = toml::from_str(config.as_str())
|
||||||
.with_context(|| format!("could not parse config: {:?}", cli.config_path()))?;
|
.with_context(|| format!("could not parse config: {}", cli.config_path().display()))?;
|
||||||
|
|
||||||
let database_path = cli.database_path()?;
|
let database_path = cli.database_path()?;
|
||||||
|
let database_path = Path::new(&database_path);
|
||||||
|
|
||||||
let config = tmdb::Config::new(config.tmdb().bearer_token().to_owned());
|
let config = tmdb::Config::new(config.tmdb().bearer_token().to_owned());
|
||||||
let cache = Rc::new(RedbCache::new(cli.cache_path())?);
|
let cache = Arc::new(RedbCache::new(cli.cache_path())?);
|
||||||
let client = Client::new(config, cache, CachePolicy::Full);
|
let client = Client::new(config, cache, CachePolicy::Full);
|
||||||
|
|
||||||
if cli.trace {
|
if cli.trace() {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
.with_test_writer()
|
.with_test_writer()
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
match cli.command() {
|
match cli.get_command() {
|
||||||
Command::Init => exec_init(database_path).await?,
|
Command::Init => exec_init(database_path).await?,
|
||||||
Command::Add { command, overrides } => {
|
Command::Add { command, overrides } => {
|
||||||
exec_add(client, database_path, command, overrides).await?
|
exec_add(client, database_path, command, overrides).await?;
|
||||||
}
|
}
|
||||||
Command::Update { command } => exec_update(client, database_path, command).await?,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exec_init(database_path: String) -> Result<()> {
|
/// Execute the `init` command.
|
||||||
db::open_new(database_path).await?;
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Forwards any errors.
|
||||||
|
async fn exec_init(database_path: &Path) -> Result<()> {
|
||||||
|
drop(db::open_new(database_path).await?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute the `add` command.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Forwards any errors.
|
||||||
async fn exec_add(
|
async fn exec_add(
|
||||||
client: Client,
|
client: Client,
|
||||||
database_path: String,
|
database_path: &Path,
|
||||||
command: AddCommand,
|
command: AddCommand,
|
||||||
overrides: AddOverrides,
|
overrides: AddOverrides,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -77,15 +91,3 @@ async fn exec_add(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exec_update(client: Client, database_path: String, command: UpdateCommand) -> Result<()> {
|
|
||||||
let database = db::open(database_path).await?;
|
|
||||||
|
|
||||||
match command {
|
|
||||||
UpdateCommand::Tmdb { command } => {
|
|
||||||
run::tmdb::update(client, database.as_ref(), command).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
+24
-13
@@ -1,3 +1,5 @@
|
|||||||
|
//! The `flix` runtime backend.
|
||||||
|
|
||||||
use flix::db::entity;
|
use flix::db::entity;
|
||||||
use flix::model::id::{CollectionId, ShowId};
|
use flix::model::id::{CollectionId, ShowId};
|
||||||
use flix::model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix::model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
@@ -5,12 +7,19 @@ use flix::model::text;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use sea_orm::ActiveValue::{NotSet, Set};
|
use sea_orm::ActiveValue::{NotSet, Set};
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr, TransactionError, TransactionTrait};
|
use sea_orm::{
|
||||||
|
ActiveModelTrait as _, DatabaseConnection, DbErr, TransactionError, TransactionTrait as _,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::cli::AddOverrides;
|
use crate::cli::AddOverrides;
|
||||||
use crate::cli::flix::AddCommand;
|
use crate::cli::flix::AddCommand;
|
||||||
|
|
||||||
pub async fn add(
|
/// Execute an `add` command.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Forwards any database transaction failures.
|
||||||
|
#[expect(clippy::print_stdout, reason = "we want to print in a binary")]
|
||||||
|
pub(crate) async fn add(
|
||||||
db: &DatabaseConnection,
|
db: &DatabaseConnection,
|
||||||
command: AddCommand,
|
command: AddCommand,
|
||||||
overrides: AddOverrides,
|
overrides: AddOverrides,
|
||||||
@@ -50,15 +59,16 @@ pub async fn add(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
println!("Created Collection: {}", title);
|
}
|
||||||
|
println!("Created Collection: {title}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
AddCommand::Episode {
|
AddCommand::Episode {
|
||||||
show_slug,
|
show_web_slug,
|
||||||
season_number,
|
season_number,
|
||||||
episode_number,
|
episode_number,
|
||||||
title,
|
title,
|
||||||
@@ -70,11 +80,11 @@ pub async fn add(
|
|||||||
let title = overrides.title.unwrap_or_else(|| title.clone());
|
let title = overrides.title.unwrap_or_else(|| title.clone());
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let show = entity::info::shows::Entity::find_by_web_slug(&show_slug)
|
let show = entity::info::shows::Entity::find_by_web_slug(&show_web_slug)
|
||||||
.one(txn)
|
.one(txn)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
DbErr::Custom(format!("show '{}' does not exist", show_slug))
|
DbErr::Custom(format!("show '{show_web_slug}' does not exist"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let flix = entity::info::episodes::ActiveModel {
|
let flix = entity::info::episodes::ActiveModel {
|
||||||
@@ -95,10 +105,11 @@ pub async fn add(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
println!("Created Episode: {}", title);
|
}
|
||||||
|
println!("Created Episode: {title}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
pub mod flix;
|
//! Various runtime backends.
|
||||||
pub mod tmdb;
|
|
||||||
|
pub(crate) mod flix;
|
||||||
|
pub(crate) mod tmdb;
|
||||||
|
|||||||
+92
-623
@@ -1,4 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
//! The `flix` runtime backend.
|
||||||
|
|
||||||
|
use alloc::collections::BTreeMap;
|
||||||
|
|
||||||
use flix::db::entity;
|
use flix::db::entity;
|
||||||
use flix::model::id::{CollectionId, MovieId, ShowId};
|
use flix::model::id::{CollectionId, MovieId, ShowId};
|
||||||
@@ -9,17 +11,24 @@ use flix::tmdb::model::id::{
|
|||||||
CollectionId as TmdbCollectionId, MovieId as TmdbMovieId, ShowId as TmdbShowId,
|
CollectionId as TmdbCollectionId, MovieId as TmdbMovieId, ShowId as TmdbShowId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use chrono::{Datelike, Utc};
|
use chrono::{Datelike as _, Utc};
|
||||||
use sea_orm::ActiveValue::{NotSet, Set};
|
use sea_orm::ActiveValue::{NotSet, Set};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, DatabaseConnection, DbErr, EntityTrait, TransactionError, TransactionTrait,
|
ActiveModelTrait as _, DatabaseConnection, DbErr, EntityTrait as _, TransactionError,
|
||||||
|
TransactionTrait as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::cli::AddOverrides;
|
use crate::cli::AddOverrides;
|
||||||
use crate::cli::tmdb::Command;
|
use crate::cli::tmdb::Command;
|
||||||
|
|
||||||
pub async fn add(
|
/// Execute an `add` command.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Forwards any database transaction failures or TMDB API failures.
|
||||||
|
#[expect(clippy::print_stderr, reason = "we want to print in a binary")]
|
||||||
|
#[expect(clippy::print_stdout, reason = "we want to print in a binary")]
|
||||||
|
pub(crate) async fn add(
|
||||||
client: Client,
|
client: Client,
|
||||||
db: &DatabaseConnection,
|
db: &DatabaseConnection,
|
||||||
command: Command,
|
command: Command,
|
||||||
@@ -54,6 +63,7 @@ pub async fn add(
|
|||||||
.web_slug
|
.web_slug
|
||||||
.unwrap_or_else(|| text::make_web_slug(&title));
|
.unwrap_or_else(|| text::make_web_slug(&title));
|
||||||
|
|
||||||
|
{
|
||||||
const COLLECTION_SUFFIX_TO_REMOVE: &str = "-collection";
|
const COLLECTION_SUFFIX_TO_REMOVE: &str = "-collection";
|
||||||
if web_slug.ends_with(COLLECTION_SUFFIX_TO_REMOVE) {
|
if web_slug.ends_with(COLLECTION_SUFFIX_TO_REMOVE) {
|
||||||
web_slug.truncate(
|
web_slug.truncate(
|
||||||
@@ -62,6 +72,7 @@ pub async fn add(
|
|||||||
.saturating_sub(COLLECTION_SUFFIX_TO_REMOVE.len()),
|
.saturating_sub(COLLECTION_SUFFIX_TO_REMOVE.len()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result: Result<CollectionId, TransactionError<DbErr>> = db
|
let result: Result<CollectionId, TransactionError<DbErr>> = db
|
||||||
.transaction(|txn| {
|
.transaction(|txn| {
|
||||||
@@ -78,7 +89,7 @@ pub async fn add(
|
|||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
entity::tmdb::collections::ActiveModel {
|
_ = entity::tmdb::collections::ActiveModel {
|
||||||
tmdb_id: Set(id),
|
tmdb_id: Set(id),
|
||||||
flix_id: Set(flix.id),
|
flix_id: Set(flix.id),
|
||||||
last_update: Set(Utc::now()),
|
last_update: Set(Utc::now()),
|
||||||
@@ -94,10 +105,11 @@ pub async fn add(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
println!("Created Collection: {}", title);
|
}
|
||||||
|
println!("Created Collection: {title}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -145,7 +157,7 @@ pub async fn add(
|
|||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
entity::tmdb::movies::ActiveModel {
|
_ = entity::tmdb::movies::ActiveModel {
|
||||||
tmdb_id: Set(id),
|
tmdb_id: Set(id),
|
||||||
flix_id: Set(flix.id),
|
flix_id: Set(flix.id),
|
||||||
last_update: Set(Utc::now()),
|
last_update: Set(Utc::now()),
|
||||||
@@ -162,10 +174,11 @@ pub async fn add(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
println!("Created Movie: {} ({})", title, year);
|
}
|
||||||
|
println!("Created Movie: {title} ({year})");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -183,7 +196,7 @@ pub async fn add(
|
|||||||
.await
|
.await
|
||||||
.with_context(|| format!("shows().get_details({})", id.into_raw()))?;
|
.with_context(|| format!("shows().get_details({})", id.into_raw()))?;
|
||||||
let mut seasons = Vec::new();
|
let mut seasons = Vec::new();
|
||||||
let mut episodes = HashMap::new();
|
let mut episodes = BTreeMap::new();
|
||||||
|
|
||||||
for season in 1..=show.number_of_seasons {
|
for season in 1..=show.number_of_seasons {
|
||||||
let season = SeasonNumber::new(season);
|
let season = SeasonNumber::new(season);
|
||||||
@@ -191,12 +204,11 @@ pub async fn add(
|
|||||||
.seasons()
|
.seasons()
|
||||||
.get_details(id, season, None)
|
.get_details(id, season, None)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| format!("seasons().get_details({}, {season})", id.into_raw()))
|
||||||
format!("seasons().get_details({}, {})", id.into_raw(), season)
|
{
|
||||||
}) {
|
|
||||||
Ok(season) => season,
|
Ok(season) => season,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err:?}");
|
eprintln!("{err}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -227,11 +239,9 @@ pub async fn add(
|
|||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"skipping episode ({}, {}, {}) - {}",
|
"skipping episode ({}, {}, {episode}) - {err}",
|
||||||
id.into_raw(),
|
id.into_raw(),
|
||||||
season.season_number,
|
season.season_number,
|
||||||
episode,
|
|
||||||
err
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -239,7 +249,7 @@ pub async fn add(
|
|||||||
season_episodes.push(episode);
|
season_episodes.push(episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
episodes.insert(season.season_number, season_episodes);
|
drop(episodes.insert(season.season_number, season_episodes));
|
||||||
seasons.push(season);
|
seasons.push(season);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +283,7 @@ pub async fn add(
|
|||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
entity::tmdb::shows::ActiveModel {
|
_ = entity::tmdb::shows::ActiveModel {
|
||||||
tmdb_id: Set(id),
|
tmdb_id: Set(id),
|
||||||
flix_id: Set(flix.id),
|
flix_id: Set(flix.id),
|
||||||
last_update: Set(Utc::now()),
|
last_update: Set(Utc::now()),
|
||||||
@@ -283,6 +293,7 @@ pub async fn add(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for season in seasons {
|
for season in seasons {
|
||||||
|
drop(
|
||||||
entity::info::seasons::ActiveModel {
|
entity::info::seasons::ActiveModel {
|
||||||
show_id: Set(flix.id),
|
show_id: Set(flix.id),
|
||||||
season_number: Set(season.season_number),
|
season_number: Set(season.season_number),
|
||||||
@@ -291,9 +302,10 @@ pub async fn add(
|
|||||||
date: Set(season.air_date),
|
date: Set(season.air_date),
|
||||||
}
|
}
|
||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
entity::tmdb::seasons::ActiveModel {
|
_ = entity::tmdb::seasons::ActiveModel {
|
||||||
tmdb_show: Set(id),
|
tmdb_show: Set(id),
|
||||||
tmdb_season: Set(season.season_number),
|
tmdb_season: Set(season.season_number),
|
||||||
flix_show: Set(flix.id),
|
flix_show: Set(flix.id),
|
||||||
@@ -306,6 +318,7 @@ pub async fn add(
|
|||||||
|
|
||||||
for (season, episodes) in episodes {
|
for (season, episodes) in episodes {
|
||||||
for episode in episodes {
|
for episode in episodes {
|
||||||
|
drop(
|
||||||
entity::info::episodes::ActiveModel {
|
entity::info::episodes::ActiveModel {
|
||||||
show_id: Set(flix.id),
|
show_id: Set(flix.id),
|
||||||
season_number: Set(season),
|
season_number: Set(season),
|
||||||
@@ -315,9 +328,10 @@ pub async fn add(
|
|||||||
date: Set(episode.air_date),
|
date: Set(episode.air_date),
|
||||||
}
|
}
|
||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
entity::tmdb::episodes::ActiveModel {
|
_ = entity::tmdb::episodes::ActiveModel {
|
||||||
tmdb_show: Set(id),
|
tmdb_show: Set(id),
|
||||||
tmdb_season: Set(season),
|
tmdb_season: Set(season),
|
||||||
tmdb_episode: Set(episode.episode_number),
|
tmdb_episode: Set(episode.episode_number),
|
||||||
@@ -339,10 +353,11 @@ pub async fn add(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
println!("Created Show: {} ({})", title, year);
|
}
|
||||||
|
println!("Created Show: {title} ({year})");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -366,11 +381,7 @@ pub async fn add(
|
|||||||
.get_details(id, season_number, None)
|
.get_details(id, season_number, None)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!("seasons().get_details({}, {season_number})", id.into_raw())
|
||||||
"seasons().get_details({}, {})",
|
|
||||||
id.into_raw(),
|
|
||||||
season_number
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
let mut episodes = Vec::new();
|
let mut episodes = Vec::new();
|
||||||
|
|
||||||
@@ -391,11 +402,9 @@ pub async fn add(
|
|||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"skipping episode ({}, {}, {}) - {}",
|
"skipping episode ({}, {}, {episode}) - {err}",
|
||||||
id.into_raw(),
|
id.into_raw(),
|
||||||
season.season_number,
|
season.season_number,
|
||||||
episode,
|
|
||||||
err
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -406,6 +415,7 @@ pub async fn add(
|
|||||||
let result: Result<(), TransactionError<DbErr>> = db
|
let result: Result<(), TransactionError<DbErr>> = db
|
||||||
.transaction(|txn| {
|
.transaction(|txn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
drop(
|
||||||
entity::info::seasons::ActiveModel {
|
entity::info::seasons::ActiveModel {
|
||||||
show_id: Set(show.flix_id),
|
show_id: Set(show.flix_id),
|
||||||
season_number: Set(season_number),
|
season_number: Set(season_number),
|
||||||
@@ -414,9 +424,10 @@ pub async fn add(
|
|||||||
date: Set(season.air_date),
|
date: Set(season.air_date),
|
||||||
}
|
}
|
||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
entity::tmdb::seasons::ActiveModel {
|
_ = entity::tmdb::seasons::ActiveModel {
|
||||||
tmdb_show: Set(show.tmdb_id),
|
tmdb_show: Set(show.tmdb_id),
|
||||||
tmdb_season: Set(season_number),
|
tmdb_season: Set(season_number),
|
||||||
flix_show: Set(show.flix_id),
|
flix_show: Set(show.flix_id),
|
||||||
@@ -427,6 +438,7 @@ pub async fn add(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for episode in episodes {
|
for episode in episodes {
|
||||||
|
drop(
|
||||||
entity::info::episodes::ActiveModel {
|
entity::info::episodes::ActiveModel {
|
||||||
show_id: Set(show.flix_id),
|
show_id: Set(show.flix_id),
|
||||||
season_number: Set(season_number),
|
season_number: Set(season_number),
|
||||||
@@ -436,9 +448,10 @@ pub async fn add(
|
|||||||
date: Set(episode.air_date),
|
date: Set(episode.air_date),
|
||||||
}
|
}
|
||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
entity::tmdb::episodes::ActiveModel {
|
_ = entity::tmdb::episodes::ActiveModel {
|
||||||
tmdb_show: Set(show.tmdb_id),
|
tmdb_show: Set(show.tmdb_id),
|
||||||
tmdb_season: Set(season_number),
|
tmdb_season: Set(season_number),
|
||||||
tmdb_episode: Set(episode.episode_number),
|
tmdb_episode: Set(episode.episode_number),
|
||||||
@@ -458,14 +471,14 @@ pub async fn add(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(()) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"Created Season: {} S{}",
|
"Created Season: {} S{season_number}",
|
||||||
show.flix_id.into_raw(),
|
show.flix_id.into_raw(),
|
||||||
season_number
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -476,19 +489,10 @@ pub async fn add(
|
|||||||
episode,
|
episode,
|
||||||
episodes,
|
episodes,
|
||||||
} => {
|
} => {
|
||||||
let id = TmdbShowId::from_raw(id);
|
/// Fetch and store episode information.
|
||||||
let season_number = season;
|
///
|
||||||
|
/// # Errors
|
||||||
let Some(show) = entity::tmdb::shows::Entity::find_by_id(id).one(db).await? else {
|
/// Forwards any database transaction failures or TMDB API failures.
|
||||||
bail!("show does not exists");
|
|
||||||
};
|
|
||||||
let Some(_) = entity::tmdb::seasons::Entity::find_by_id((id, season))
|
|
||||||
.one(db)
|
|
||||||
.await?
|
|
||||||
else {
|
|
||||||
bail!("season does not exists");
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn fetch_episode(
|
async fn fetch_episode(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
db: &DatabaseConnection,
|
db: &DatabaseConnection,
|
||||||
@@ -512,12 +516,13 @@ pub async fn add(
|
|||||||
.get_details(id, season, episode_number, None)
|
.get_details(id, season, episode_number, None)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("episodes().get_details({}, {})", id.into_raw(), season)
|
format!("episodes().get_details({}, {season})", id.into_raw())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let result: Result<(), TransactionError<DbErr>> = db
|
let result: Result<(), TransactionError<DbErr>> = db
|
||||||
.transaction(|txn| {
|
.transaction(|txn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
drop(
|
||||||
entity::info::episodes::ActiveModel {
|
entity::info::episodes::ActiveModel {
|
||||||
show_id: Set(flix_id),
|
show_id: Set(flix_id),
|
||||||
season_number: Set(season),
|
season_number: Set(season),
|
||||||
@@ -527,9 +532,10 @@ pub async fn add(
|
|||||||
date: Set(episode.air_date),
|
date: Set(episode.air_date),
|
||||||
}
|
}
|
||||||
.insert(txn)
|
.insert(txn)
|
||||||
.await?;
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
entity::tmdb::episodes::ActiveModel {
|
_ = entity::tmdb::episodes::ActiveModel {
|
||||||
tmdb_show: Set(tmdb_id),
|
tmdb_show: Set(tmdb_id),
|
||||||
tmdb_season: Set(season),
|
tmdb_season: Set(season),
|
||||||
tmdb_episode: Set(episode_number),
|
tmdb_episode: Set(episode_number),
|
||||||
@@ -548,20 +554,32 @@ pub async fn add(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(()) => {}
|
||||||
Err(TransactionError::Connection(err)) => Err(err)?,
|
Err(TransactionError::Connection(err) | TransactionError::Transaction(err)) => {
|
||||||
Err(TransactionError::Transaction(err)) => Err(err)?,
|
return Err(err.into());
|
||||||
};
|
}
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"Created Episode: {} S{}E{}",
|
"Created Episode: {} S{season}E{episode_number}",
|
||||||
flix_id.into_raw(),
|
flix_id.into_raw(),
|
||||||
season,
|
|
||||||
episode_number
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let id = TmdbShowId::from_raw(id);
|
||||||
|
let season_number = season;
|
||||||
|
|
||||||
|
let Some(show) = entity::tmdb::shows::Entity::find_by_id(id).one(db).await? else {
|
||||||
|
bail!("show does not exists");
|
||||||
|
};
|
||||||
|
let Some(_) = entity::tmdb::seasons::Entity::find_by_id((id, season))
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
bail!("season does not exists");
|
||||||
|
};
|
||||||
|
|
||||||
let flix_id = show.flix_id;
|
let flix_id = show.flix_id;
|
||||||
let tmdb_id = show.tmdb_id;
|
let tmdb_id = show.tmdb_id;
|
||||||
|
|
||||||
@@ -574,552 +592,3 @@ pub async fn add(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(client: Client, db: &DatabaseConnection, command: Command) -> Result<()> {
|
|
||||||
_ = client;
|
|
||||||
_ = db;
|
|
||||||
_ = command;
|
|
||||||
unimplemented!("updates")
|
|
||||||
|
|
||||||
// match command {
|
|
||||||
// Command::Collection { id } => {
|
|
||||||
// let id = TmdbCollectionId::from_raw(id);
|
|
||||||
|
|
||||||
// let collection = entity::tmdb::collections::Entity::find_by_id(id)
|
|
||||||
// .one(db)
|
|
||||||
// .await?;
|
|
||||||
// if collection.is_some() {
|
|
||||||
// bail!("collection already exists");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let collection = client
|
|
||||||
// .collections()
|
|
||||||
// .get_details(id, None)
|
|
||||||
// .await
|
|
||||||
// .with_context(|| format!("collections().get_details({})", id.into_raw()))?;
|
|
||||||
|
|
||||||
// let title = overrides.title.unwrap_or(collection.title);
|
|
||||||
|
|
||||||
// let sort_title = overrides
|
|
||||||
// .sort_title
|
|
||||||
// .unwrap_or_else(|| text::make_sortable_title(&title));
|
|
||||||
// let fs_slug = overrides
|
|
||||||
// .fs_slug
|
|
||||||
// .unwrap_or_else(|| text::make_fs_slug(&title));
|
|
||||||
// let web_slug = overrides
|
|
||||||
// .web_slug
|
|
||||||
// .unwrap_or_else(|| text::make_web_slug(&title));
|
|
||||||
|
|
||||||
// let result: Result<CollectionId, TransactionError<DbErr>> = db
|
|
||||||
// .transaction(|txn| {
|
|
||||||
// let title = title.clone();
|
|
||||||
// Box::pin(async move {
|
|
||||||
// let flix = entity::info::collections::ActiveModel {
|
|
||||||
// id: NotSet,
|
|
||||||
// title: Set(title),
|
|
||||||
// overview: Set(collection.overview),
|
|
||||||
// sort_title: Set(sort_title),
|
|
||||||
// fs_slug: Set(fs_slug),
|
|
||||||
// web_slug: Set(web_slug),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::collections::ActiveModel {
|
|
||||||
// tmdb_id: Set(id),
|
|
||||||
// flix_id: Set(flix.id),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// movie_count: Set(collection.movies.len().try_into().unwrap_or(0)),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// Ok(flix.id)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let flix_id = match result {
|
|
||||||
// Ok(id) => id,
|
|
||||||
// Err(TransactionError::Connection(err)) => Err(err)?,
|
|
||||||
// Err(TransactionError::Transaction(err)) => Err(err)?,
|
|
||||||
// };
|
|
||||||
// println!("Created Collection: {}", title, flix_id.into_raw());
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// Command::Movie { id } => {
|
|
||||||
// let id = TmdbMovieId::from_raw(id);
|
|
||||||
|
|
||||||
// let movie = entity::tmdb::movies::Entity::find_by_id(id).one(db).await?;
|
|
||||||
// if movie.is_some() {
|
|
||||||
// bail!("movie already exists");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let movie = client
|
|
||||||
// .movies()
|
|
||||||
// .get_details(id, None)
|
|
||||||
// .await
|
|
||||||
// .with_context(|| format!("movies().get_details({})", id.into_raw()))?;
|
|
||||||
|
|
||||||
// let title = overrides.title.unwrap_or(movie.title);
|
|
||||||
// let year = movie.release_date.year();
|
|
||||||
|
|
||||||
// let sort_title = overrides
|
|
||||||
// .sort_title
|
|
||||||
// .unwrap_or_else(|| text::make_sortable_title(&title));
|
|
||||||
// let fs_slug = overrides
|
|
||||||
// .fs_slug
|
|
||||||
// .unwrap_or_else(|| text::make_fs_slug_year(&title, year));
|
|
||||||
// let web_slug = overrides
|
|
||||||
// .web_slug
|
|
||||||
// .unwrap_or_else(|| text::make_web_slug_year(&title, year));
|
|
||||||
|
|
||||||
// let result: Result<MovieId, TransactionError<DbErr>> = db
|
|
||||||
// .transaction(|txn| {
|
|
||||||
// let title = title.clone();
|
|
||||||
// Box::pin(async move {
|
|
||||||
// let flix = entity::info::movies::ActiveModel {
|
|
||||||
// id: NotSet,
|
|
||||||
// title: Set(title),
|
|
||||||
// tagline: Set(movie.tagline),
|
|
||||||
// overview: Set(movie.overview),
|
|
||||||
// date: Set(movie.release_date),
|
|
||||||
// sort_title: Set(sort_title),
|
|
||||||
// fs_slug: Set(fs_slug),
|
|
||||||
// web_slug: Set(web_slug),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::movies::ActiveModel {
|
|
||||||
// tmdb_id: Set(id),
|
|
||||||
// flix_id: Set(flix.id),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// runtime: Set(movie.runtime.into()),
|
|
||||||
// collection_id: Set(movie.collection.map(|c| c.id)),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// Ok(flix.id)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let flix_id = match result {
|
|
||||||
// Ok(id) => id,
|
|
||||||
// Err(TransactionError::Connection(err)) => Err(err)?,
|
|
||||||
// Err(TransactionError::Transaction(err)) => Err(err)?,
|
|
||||||
// };
|
|
||||||
// println!(
|
|
||||||
// "Created Movie: {} ({})",
|
|
||||||
// title,
|
|
||||||
// year,
|
|
||||||
// flix_id.into_raw(),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// Command::Show { id } => {
|
|
||||||
// let id = TmdbShowId::from_raw(id);
|
|
||||||
|
|
||||||
// let show = entity::tmdb::shows::Entity::find_by_id(id).one(db).await?;
|
|
||||||
// if show.is_some() {
|
|
||||||
// bail!("show already exists");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let show = client
|
|
||||||
// .shows()
|
|
||||||
// .get_details(id, None)
|
|
||||||
// .await
|
|
||||||
// .with_context(|| format!("shows().get_details({})", id.into_raw()))?;
|
|
||||||
// let mut seasons = Vec::new();
|
|
||||||
// let mut episodes = HashMap::new();
|
|
||||||
|
|
||||||
// for season in 1..=show.number_of_seasons {
|
|
||||||
// let season = SeasonNumber::new(season);
|
|
||||||
// let season = match client
|
|
||||||
// .seasons()
|
|
||||||
// .get_details(id, season, None)
|
|
||||||
// .await
|
|
||||||
// .with_context(|| {
|
|
||||||
// format!("seasons().get_details({}, {})", id.into_raw(), season)
|
|
||||||
// }) {
|
|
||||||
// Ok(season) => season,
|
|
||||||
// Err(err) => {
|
|
||||||
// eprintln!("{err:?}");
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// if season.air_date > Utc::now().naive_utc().date() {
|
|
||||||
// eprintln!(
|
|
||||||
// "skipping season ({}, {})",
|
|
||||||
// id.into_raw(),
|
|
||||||
// season.season_number
|
|
||||||
// );
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let Ok(number_of_episodes) = u32::try_from(season.episodes.len()) else {
|
|
||||||
// bail!(
|
|
||||||
// "could not convert {} to an EpisodeNumber",
|
|
||||||
// season.episodes.len()
|
|
||||||
// )
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let mut season_episodes = Vec::new();
|
|
||||||
// for episode in 1..=number_of_episodes {
|
|
||||||
// let episode = EpisodeNumber::new(episode);
|
|
||||||
// let Ok(episode) = client
|
|
||||||
// .episodes()
|
|
||||||
// .get_details(id, season.season_number, episode, None)
|
|
||||||
// .await
|
|
||||||
// else {
|
|
||||||
// eprintln!(
|
|
||||||
// "skipping episode ({}, {}, {})",
|
|
||||||
// id.into_raw(),
|
|
||||||
// season.season_number,
|
|
||||||
// episode
|
|
||||||
// );
|
|
||||||
// break;
|
|
||||||
// };
|
|
||||||
// season_episodes.push(episode);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// episodes.insert(season.season_number, season_episodes);
|
|
||||||
// seasons.push(season);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let title = overrides.title.unwrap_or(show.title);
|
|
||||||
// let year = show.first_air_date.year();
|
|
||||||
|
|
||||||
// let sort_title = overrides
|
|
||||||
// .sort_title
|
|
||||||
// .unwrap_or_else(|| text::make_sortable_title(&title));
|
|
||||||
// let fs_slug = overrides
|
|
||||||
// .fs_slug
|
|
||||||
// .unwrap_or_else(|| text::make_fs_slug_year(&title, year));
|
|
||||||
// let web_slug = overrides
|
|
||||||
// .web_slug
|
|
||||||
// .unwrap_or_else(|| text::make_web_slug_year(&title, year));
|
|
||||||
|
|
||||||
// let result: Result<ShowId, TransactionError<DbErr>> = db
|
|
||||||
// .transaction(|txn| {
|
|
||||||
// let title = title.clone();
|
|
||||||
// Box::pin(async move {
|
|
||||||
// let flix = entity::info::shows::ActiveModel {
|
|
||||||
// id: NotSet,
|
|
||||||
// title: Set(title),
|
|
||||||
// tagline: Set(show.tagline),
|
|
||||||
// overview: Set(show.overview),
|
|
||||||
// date: Set(show.first_air_date),
|
|
||||||
// sort_title: Set(sort_title),
|
|
||||||
// fs_slug: Set(fs_slug),
|
|
||||||
// web_slug: Set(web_slug),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::shows::ActiveModel {
|
|
||||||
// tmdb_id: Set(id),
|
|
||||||
// flix_id: Set(flix.id),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// number_of_seasons: Set(show.number_of_seasons),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// for season in seasons {
|
|
||||||
// entity::info::seasons::ActiveModel {
|
|
||||||
// show_id: Set(flix.id),
|
|
||||||
// season_number: Set(season.season_number),
|
|
||||||
// title: Set(season.title),
|
|
||||||
// overview: Set(season.overview),
|
|
||||||
// date: Set(season.air_date),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::seasons::ActiveModel {
|
|
||||||
// tmdb_show: Set(id),
|
|
||||||
// tmdb_season: Set(season.season_number),
|
|
||||||
// flix_show: Set(flix.id),
|
|
||||||
// flix_season: Set(season.season_number),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (season, episodes) in episodes {
|
|
||||||
// for episode in episodes {
|
|
||||||
// entity::info::episodes::ActiveModel {
|
|
||||||
// show_id: Set(flix.id),
|
|
||||||
// season_number: Set(season),
|
|
||||||
// episode_number: Set(episode.episode_number),
|
|
||||||
// title: Set(episode.title),
|
|
||||||
// overview: Set(episode.overview),
|
|
||||||
// date: Set(episode.air_date),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::episodes::ActiveModel {
|
|
||||||
// tmdb_show: Set(id),
|
|
||||||
// tmdb_season: Set(season),
|
|
||||||
// tmdb_episode: Set(episode.episode_number),
|
|
||||||
// flix_show: Set(flix.id),
|
|
||||||
// flix_season: Set(season),
|
|
||||||
// flix_episode: Set(episode.episode_number),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// runtime: Set(episode.runtime.into()),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(flix.id)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let flix_id = match result {
|
|
||||||
// Ok(id) => id,
|
|
||||||
// Err(TransactionError::Connection(err)) => Err(err)?,
|
|
||||||
// Err(TransactionError::Transaction(err)) => Err(err)?,
|
|
||||||
// };
|
|
||||||
// println!(
|
|
||||||
// "Created Show: {} ({})",
|
|
||||||
// title,
|
|
||||||
// year,
|
|
||||||
// flix_id.into_raw()
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// Command::Season { id, season } => {
|
|
||||||
// let id = TmdbShowId::from_raw(id);
|
|
||||||
// let season_number = season;
|
|
||||||
|
|
||||||
// let Some(show) = entity::tmdb::shows::Entity::find_by_id(id).one(db).await? else {
|
|
||||||
// bail!("show does not exists");
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let season = entity::tmdb::seasons::Entity::find_by_id((id, season))
|
|
||||||
// .one(db)
|
|
||||||
// .await?;
|
|
||||||
// if season.is_some() {
|
|
||||||
// bail!("season already exists");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let season = client
|
|
||||||
// .seasons()
|
|
||||||
// .get_details(id, season_number, None)
|
|
||||||
// .await
|
|
||||||
// .with_context(|| {
|
|
||||||
// format!(
|
|
||||||
// "seasons().get_details({}, {})",
|
|
||||||
// id.into_raw(),
|
|
||||||
// season_number
|
|
||||||
// )
|
|
||||||
// })?;
|
|
||||||
// let mut episodes = Vec::new();
|
|
||||||
|
|
||||||
// let Ok(number_of_episodes) = u32::try_from(season.episodes.len()) else {
|
|
||||||
// bail!(
|
|
||||||
// "could not convert {} to an EpisodeNumber",
|
|
||||||
// season.episodes.len()
|
|
||||||
// )
|
|
||||||
// };
|
|
||||||
|
|
||||||
// for episode in 1..=number_of_episodes {
|
|
||||||
// let episode = EpisodeNumber::new(episode);
|
|
||||||
// let Ok(episode) = client
|
|
||||||
// .episodes()
|
|
||||||
// .get_details(id, season.season_number, episode, None)
|
|
||||||
// .await
|
|
||||||
// else {
|
|
||||||
// eprintln!(
|
|
||||||
// "skipping episode ({}, {}, {})",
|
|
||||||
// id.into_raw(),
|
|
||||||
// season.season_number,
|
|
||||||
// episode
|
|
||||||
// );
|
|
||||||
// break;
|
|
||||||
// };
|
|
||||||
// episodes.push(episode);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let result: Result<(), TransactionError<DbErr>> = db
|
|
||||||
// .transaction(|txn| {
|
|
||||||
// Box::pin(async move {
|
|
||||||
// entity::info::seasons::ActiveModel {
|
|
||||||
// show_id: Set(show.flix_id),
|
|
||||||
// season_number: Set(season_number),
|
|
||||||
// title: Set(season.title),
|
|
||||||
// overview: Set(season.overview),
|
|
||||||
// date: Set(season.air_date),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::seasons::ActiveModel {
|
|
||||||
// tmdb_show: Set(show.tmdb_id),
|
|
||||||
// tmdb_season: Set(season_number),
|
|
||||||
// flix_show: Set(show.flix_id),
|
|
||||||
// flix_season: Set(season_number),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// for episode in episodes {
|
|
||||||
// entity::info::episodes::ActiveModel {
|
|
||||||
// show_id: Set(show.flix_id),
|
|
||||||
// season_number: Set(season_number),
|
|
||||||
// episode_number: Set(episode.episode_number),
|
|
||||||
// title: Set(episode.title),
|
|
||||||
// overview: Set(episode.overview),
|
|
||||||
// date: Set(episode.air_date),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::episodes::ActiveModel {
|
|
||||||
// tmdb_show: Set(show.tmdb_id),
|
|
||||||
// tmdb_season: Set(season_number),
|
|
||||||
// tmdb_episode: Set(episode.episode_number),
|
|
||||||
// flix_show: Set(show.flix_id),
|
|
||||||
// flix_season: Set(season_number),
|
|
||||||
// flix_episode: Set(episode.episode_number),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// runtime: Set(episode.runtime.into()),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// match result {
|
|
||||||
// Ok(_) => (),
|
|
||||||
// Err(TransactionError::Connection(err)) => Err(err)?,
|
|
||||||
// Err(TransactionError::Transaction(err)) => Err(err)?,
|
|
||||||
// };
|
|
||||||
// println!(
|
|
||||||
// "Created Season: {} S{}",
|
|
||||||
// show.flix_id.into_raw(),
|
|
||||||
// season_number
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// Command::Episode {
|
|
||||||
// id,
|
|
||||||
// season,
|
|
||||||
// episode,
|
|
||||||
// episodes,
|
|
||||||
// } => {
|
|
||||||
// let id = TmdbShowId::from_raw(id);
|
|
||||||
// let season_number = season;
|
|
||||||
|
|
||||||
// let Some(show) = entity::tmdb::shows::Entity::find_by_id(id).one(db).await? else {
|
|
||||||
// bail!("show does not exists");
|
|
||||||
// };
|
|
||||||
// let Some(_) = entity::tmdb::seasons::Entity::find_by_id((id, season))
|
|
||||||
// .one(db)
|
|
||||||
// .await?
|
|
||||||
// else {
|
|
||||||
// bail!("season does not exists");
|
|
||||||
// };
|
|
||||||
|
|
||||||
// async fn fetch_episode(
|
|
||||||
// client: &Client,
|
|
||||||
// db: &DatabaseConnection,
|
|
||||||
// flix_id: ShowId,
|
|
||||||
// tmdb_id: TmdbShowId,
|
|
||||||
// id: TmdbShowId,
|
|
||||||
// season: SeasonNumber,
|
|
||||||
// episode: EpisodeNumber,
|
|
||||||
// ) -> Result<()> {
|
|
||||||
// let episode_number = episode;
|
|
||||||
|
|
||||||
// let episode = entity::tmdb::episodes::Entity::find_by_id((id, season, episode))
|
|
||||||
// .one(db)
|
|
||||||
// .await?;
|
|
||||||
// if episode.is_some() {
|
|
||||||
// bail!("episode already exists");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let episode = client
|
|
||||||
// .episodes()
|
|
||||||
// .get_details(id, season, episode_number, None)
|
|
||||||
// .await
|
|
||||||
// .with_context(|| {
|
|
||||||
// format!("episodes().get_details({}, {})", id.into_raw(), season)
|
|
||||||
// })?;
|
|
||||||
|
|
||||||
// let result: Result<(), TransactionError<DbErr>> = db
|
|
||||||
// .transaction(|txn| {
|
|
||||||
// Box::pin(async move {
|
|
||||||
// entity::info::episodes::ActiveModel {
|
|
||||||
// show_id: Set(flix_id),
|
|
||||||
// season_number: Set(season),
|
|
||||||
// episode_number: Set(episode_number),
|
|
||||||
// title: Set(episode.title),
|
|
||||||
// overview: Set(episode.overview),
|
|
||||||
// date: Set(episode.air_date),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// entity::tmdb::episodes::ActiveModel {
|
|
||||||
// tmdb_show: Set(tmdb_id),
|
|
||||||
// tmdb_season: Set(season),
|
|
||||||
// tmdb_episode: Set(episode_number),
|
|
||||||
// flix_show: Set(flix_id),
|
|
||||||
// flix_season: Set(season),
|
|
||||||
// flix_episode: Set(episode_number),
|
|
||||||
// last_update: Set(Utc::now()),
|
|
||||||
// runtime: Set(episode.runtime.into()),
|
|
||||||
// }
|
|
||||||
// .insert(txn)
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// match result {
|
|
||||||
// Ok(_) => (),
|
|
||||||
// Err(TransactionError::Connection(err)) => Err(err)?,
|
|
||||||
// Err(TransactionError::Transaction(err)) => Err(err)?,
|
|
||||||
// };
|
|
||||||
// println!(
|
|
||||||
// "Created Episode: {} S{}E{}",
|
|
||||||
// flix_id.into_raw(),
|
|
||||||
// season,
|
|
||||||
// episode_number
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let flix_id = show.flix_id;
|
|
||||||
// let tmdb_id = show.tmdb_id;
|
|
||||||
|
|
||||||
// fetch_episode(&client, db, flix_id, tmdb_id, id, season_number, episode).await?;
|
|
||||||
// for episode in episodes {
|
|
||||||
// fetch_episode(&client, db, flix_id, tmdb_id, id, season_number, episode).await?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix-db"
|
name = "flix-db"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "Types for storing persistent data about media"
|
description = "Types for storing persistent data about media"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = []
|
keywords = ["flix"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -29,7 +31,7 @@ flix-tmdb = { workspace = true, features = ["sea-orm"], optional = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
sea-orm-migration = { workspace = true, features = ["runtime-tokio-rustls"] }
|
sea-orm-migration = { workspace = true, features = ["runtime-tokio-rustls"] }
|
||||||
tokio = { version = "^1", default-features = false, features = [
|
tokio = { workspace = true, features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rt",
|
"rt",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
//! Types and functions related to [DatabaseConnection]s
|
//! Types and functions related to [`DatabaseConnection`]s.
|
||||||
|
|
||||||
use sea_orm::{DatabaseConnection, DbErr};
|
use sea_orm::{DatabaseConnection, DbErr};
|
||||||
use sea_orm_migration::MigratorTrait as _;
|
use sea_orm_migration::MigratorTrait as _;
|
||||||
|
|
||||||
/// A newtype wrapping a [DatabaseConnection]
|
/// A newtype wrapping a [`DatabaseConnection`].
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Connection(DatabaseConnection);
|
pub struct Connection(DatabaseConnection);
|
||||||
|
|
||||||
impl Connection {
|
impl Connection {
|
||||||
/// Helper function for applying database migrations while wrapping a
|
/// Helper function for applying database migrations while wrapping a
|
||||||
/// [DatabaseConnection] in a newtype
|
/// [`DatabaseConnection`] in a newtype.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Fails if connecting or applying migrations fail.
|
||||||
|
#[inline]
|
||||||
pub async fn try_from(db: DatabaseConnection) -> Result<Self, DbErr> {
|
pub async fn try_from(db: DatabaseConnection) -> Result<Self, DbErr> {
|
||||||
|
// The migrations only create views which store no data, so it is
|
||||||
|
// important to down before up since modifications are impossible.
|
||||||
|
//
|
||||||
|
// Syncing the schema registry allows all real tables to be upgraded.
|
||||||
|
// It is important to sync twice to ensure internal consistency so that
|
||||||
|
// the views can be recreated on the latest schema.
|
||||||
crate::migration::Migrator::down(&db, None).await?;
|
crate::migration::Migrator::down(&db, None).await?;
|
||||||
db.get_schema_registry("flix_db::*").sync(&db).await?;
|
db.get_schema_registry("flix_db::*").sync(&db).await?;
|
||||||
db.get_schema_registry("flix_db::*").sync(&db).await?;
|
db.get_schema_registry("flix_db::*").sync(&db).await?;
|
||||||
@@ -19,6 +30,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<DatabaseConnection> for Connection {
|
impl AsRef<DatabaseConnection> for Connection {
|
||||||
|
#[inline]
|
||||||
fn as_ref(&self) -> &DatabaseConnection {
|
fn as_ref(&self) -> &DatabaseConnection {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-90
@@ -1,6 +1,6 @@
|
|||||||
//! This module contains entities for storing media file information
|
//! This module contains entities for storing media file information.
|
||||||
|
|
||||||
/// Library entity
|
/// Library entity.
|
||||||
pub mod libraries {
|
pub mod libraries {
|
||||||
use flix_model::id::LibraryId;
|
use flix_model::id::LibraryId;
|
||||||
|
|
||||||
@@ -10,42 +10,43 @@ pub mod libraries {
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
/// The database representation of a library media folder
|
/// The database representation of a library media folder.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_libraries")]
|
#[sea_orm(table_name = "flix_libraries")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The library's ID
|
/// The library's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: LibraryId,
|
pub id: LibraryId,
|
||||||
/// The library's directory
|
/// The library's directory.
|
||||||
pub directory: PathBytes,
|
pub directory: PathBytes,
|
||||||
/// The library's last scan data
|
/// The library's last scan data.
|
||||||
pub last_scan_date: Option<DateTime<Utc>>,
|
pub last_scan_date: Option<DateTime<Utc>>,
|
||||||
/// The library's last scan duration
|
/// The library's last scan duration.
|
||||||
pub last_scan_duration: Option<Seconds>,
|
pub last_scan_duration: Option<Seconds>,
|
||||||
|
|
||||||
/// Collections that are part of this library
|
/// Collections that are part of this library.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub collections: HasMany<super::collections::Entity>,
|
pub collections: HasMany<super::collections::Entity>,
|
||||||
/// Movies that are part of this library
|
/// Movies that are part of this library.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub movies: HasMany<super::movies::Entity>,
|
pub movies: HasMany<super::movies::Entity>,
|
||||||
/// Shows that are part of this library
|
/// Shows that are part of this library.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub shows: HasMany<super::shows::Entity>,
|
pub shows: HasMany<super::shows::Entity>,
|
||||||
/// Seasons that are part of this library
|
/// Seasons that are part of this library.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub seasons: HasMany<super::seasons::Entity>,
|
pub seasons: HasMany<super::seasons::Entity>,
|
||||||
/// Episodes that are part of this library
|
/// Episodes that are part of this library.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collection entity
|
/// Collection entity.
|
||||||
pub mod collections {
|
pub mod collections {
|
||||||
use flix_model::id::{CollectionId, LibraryId};
|
use flix_model::id::{CollectionId, LibraryId};
|
||||||
|
|
||||||
@@ -55,25 +56,25 @@ pub mod collections {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a collection media folder
|
/// The database representation of a collection media folder.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_collections")]
|
#[sea_orm(table_name = "flix_collections")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The collection's ID
|
/// The collection's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: CollectionId,
|
pub id: CollectionId,
|
||||||
/// The collection's parent
|
/// The collection's parent.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub parent_id: Option<CollectionId>,
|
pub parent_id: Option<CollectionId>,
|
||||||
/// The collection's library ID
|
/// The collection's library ID.
|
||||||
pub library_id: LibraryId,
|
pub library_id: LibraryId,
|
||||||
/// The collection's directory
|
/// The collection's directory.
|
||||||
pub directory: PathBytes,
|
pub directory: PathBytes,
|
||||||
/// The collection's poster path
|
/// The collection's poster path.
|
||||||
pub relative_poster_path: Option<String>,
|
pub relative_poster_path: Option<String>,
|
||||||
|
|
||||||
/// This collection's parent
|
/// This collection's parent.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
self_ref,
|
self_ref,
|
||||||
relation_enum = "Parent",
|
relation_enum = "Parent",
|
||||||
@@ -83,7 +84,7 @@ pub mod collections {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub parent: HasOne<Entity>,
|
pub parent: HasOne<Entity>,
|
||||||
/// The library this collection belongs to
|
/// The library this collection belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "library_id",
|
from = "library_id",
|
||||||
@@ -92,7 +93,7 @@ pub mod collections {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub library: HasOne<super::libraries::Entity>,
|
pub library: HasOne<super::libraries::Entity>,
|
||||||
/// The info for this collection
|
/// The info for this collection.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -103,15 +104,16 @@ pub mod collections {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::collections::Entity>,
|
pub info: HasOne<entity::info::collections::Entity>,
|
||||||
|
|
||||||
/// The watched info for this collection
|
/// The watched info for this collection.
|
||||||
#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
|
#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
|
||||||
pub watched: HasMany<entity::watched::collections::Entity>,
|
pub watched: HasMany<entity::watched::collections::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Movie entity
|
/// Movie entity.
|
||||||
pub mod movies {
|
pub mod movies {
|
||||||
use flix_model::id::{CollectionId, LibraryId, MovieId};
|
use flix_model::id::{CollectionId, LibraryId, MovieId};
|
||||||
|
|
||||||
@@ -121,27 +123,27 @@ pub mod movies {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a movie media folder
|
/// The database representation of a movie media folder.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_movies")]
|
#[sea_orm(table_name = "flix_movies")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The movie's ID
|
/// The movie's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: MovieId,
|
pub id: MovieId,
|
||||||
/// The movie's parent
|
/// The movie's parent.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub parent_id: Option<CollectionId>,
|
pub parent_id: Option<CollectionId>,
|
||||||
/// The movie's library
|
/// The movie's library.
|
||||||
pub library_id: LibraryId,
|
pub library_id: LibraryId,
|
||||||
/// The movie's directory
|
/// The movie's directory.
|
||||||
pub directory: PathBytes,
|
pub directory: PathBytes,
|
||||||
/// The movie's media path
|
/// The movie's media path.
|
||||||
pub relative_media_path: String,
|
pub relative_media_path: String,
|
||||||
/// The movie's poster path
|
/// The movie's poster path.
|
||||||
pub relative_poster_path: Option<String>,
|
pub relative_poster_path: Option<String>,
|
||||||
|
|
||||||
/// This movie's parent
|
/// This movie's parent.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "parent_id",
|
from = "parent_id",
|
||||||
@@ -150,7 +152,7 @@ pub mod movies {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub parent: HasOne<super::collections::Entity>,
|
pub parent: HasOne<super::collections::Entity>,
|
||||||
/// The library this movie belongs to
|
/// The library this movie belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "library_id",
|
from = "library_id",
|
||||||
@@ -159,7 +161,7 @@ pub mod movies {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub library: HasOne<super::libraries::Entity>,
|
pub library: HasOne<super::libraries::Entity>,
|
||||||
/// The info for this movie
|
/// The info for this movie.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -170,15 +172,16 @@ pub mod movies {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::movies::Entity>,
|
pub info: HasOne<entity::info::movies::Entity>,
|
||||||
|
|
||||||
/// The watched info for this movie
|
/// The watched info for this movie.
|
||||||
#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
|
#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
|
||||||
pub watched: HasMany<entity::watched::movies::Entity>,
|
pub watched: HasMany<entity::watched::movies::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show entity
|
/// Show entity.
|
||||||
pub mod shows {
|
pub mod shows {
|
||||||
use flix_model::id::{CollectionId, LibraryId, ShowId};
|
use flix_model::id::{CollectionId, LibraryId, ShowId};
|
||||||
|
|
||||||
@@ -188,25 +191,25 @@ pub mod shows {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a show media folder
|
/// The database representation of a show media folder.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_shows")]
|
#[sea_orm(table_name = "flix_shows")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The show's ID
|
/// The show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: ShowId,
|
pub id: ShowId,
|
||||||
/// The show's parent
|
/// The show's parent.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub parent_id: Option<CollectionId>,
|
pub parent_id: Option<CollectionId>,
|
||||||
/// The show's library
|
/// The show's library.
|
||||||
pub library_id: LibraryId,
|
pub library_id: LibraryId,
|
||||||
/// The show's directory
|
/// The show's directory.
|
||||||
pub directory: PathBytes,
|
pub directory: PathBytes,
|
||||||
/// The show's poster path
|
/// The show's poster path.
|
||||||
pub relative_poster_path: Option<String>,
|
pub relative_poster_path: Option<String>,
|
||||||
|
|
||||||
/// This show's parent
|
/// This show's parent.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "parent_id",
|
from = "parent_id",
|
||||||
@@ -215,7 +218,7 @@ pub mod shows {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub parent: HasOne<super::collections::Entity>,
|
pub parent: HasOne<super::collections::Entity>,
|
||||||
/// The library this show belongs to
|
/// The library this show belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "library_id",
|
from = "library_id",
|
||||||
@@ -224,7 +227,7 @@ pub mod shows {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub library: HasOne<super::libraries::Entity>,
|
pub library: HasOne<super::libraries::Entity>,
|
||||||
/// The info for this show
|
/// The info for this show.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -235,21 +238,22 @@ pub mod shows {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::shows::Entity>,
|
pub info: HasOne<entity::info::shows::Entity>,
|
||||||
|
|
||||||
/// Seasons that are part of this show
|
/// Seasons that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub seasons: HasMany<super::seasons::Entity>,
|
pub seasons: HasMany<super::seasons::Entity>,
|
||||||
/// Episodes that are part of this show
|
/// Episodes that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
/// The watched info for this show
|
/// The watched info for this show.
|
||||||
#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
|
#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
|
||||||
pub watched: HasMany<entity::watched::shows::Entity>,
|
pub watched: HasMany<entity::watched::shows::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Season entity
|
/// Season entity.
|
||||||
pub mod seasons {
|
pub mod seasons {
|
||||||
use flix_model::id::{LibraryId, ShowId};
|
use flix_model::id::{LibraryId, ShowId};
|
||||||
use flix_model::numbers::SeasonNumber;
|
use flix_model::numbers::SeasonNumber;
|
||||||
@@ -260,25 +264,25 @@ pub mod seasons {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a season media folder
|
/// The database representation of a season media folder.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_seasons")]
|
#[sea_orm(table_name = "flix_seasons")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The season's show's ID
|
/// The season's show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub show_id: ShowId,
|
pub show_id: ShowId,
|
||||||
/// The season's number
|
/// The season's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The season's library
|
/// The season's library.
|
||||||
pub library_id: LibraryId,
|
pub library_id: LibraryId,
|
||||||
/// The season's directory
|
/// The season's directory.
|
||||||
pub directory: PathBytes,
|
pub directory: PathBytes,
|
||||||
/// The season's poster path
|
/// The season's poster path.
|
||||||
pub relative_poster_path: Option<String>,
|
pub relative_poster_path: Option<String>,
|
||||||
|
|
||||||
/// This season's show
|
/// This season's show.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "show_id",
|
from = "show_id",
|
||||||
@@ -287,7 +291,7 @@ pub mod seasons {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub show: HasOne<super::shows::Entity>,
|
pub show: HasOne<super::shows::Entity>,
|
||||||
/// The library this season belongs to
|
/// The library this season belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "library_id",
|
from = "library_id",
|
||||||
@@ -296,7 +300,7 @@ pub mod seasons {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub library: HasOne<super::libraries::Entity>,
|
pub library: HasOne<super::libraries::Entity>,
|
||||||
/// The info for this season
|
/// The info for this season.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -307,10 +311,10 @@ pub mod seasons {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::seasons::Entity>,
|
pub info: HasOne<entity::info::seasons::Entity>,
|
||||||
|
|
||||||
/// Episodes that are part of this show
|
/// Episodes that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
/// The watched info for this season
|
/// The watched info for this season.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
has_many,
|
has_many,
|
||||||
relation_enum = "Watched",
|
relation_enum = "Watched",
|
||||||
@@ -320,10 +324,11 @@ pub mod seasons {
|
|||||||
pub watched: HasMany<entity::watched::seasons::Entity>,
|
pub watched: HasMany<entity::watched::seasons::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Episode entity
|
/// Episode entity.
|
||||||
pub mod episodes {
|
pub mod episodes {
|
||||||
use flix_model::id::{LibraryId, ShowId};
|
use flix_model::id::{LibraryId, ShowId};
|
||||||
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
@@ -334,32 +339,32 @@ pub mod episodes {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a episode media folder
|
/// The database representation of a episode media folder.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_episodes")]
|
#[sea_orm(table_name = "flix_episodes")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The episode's show's ID
|
/// The episode's show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub show_id: ShowId,
|
pub show_id: ShowId,
|
||||||
/// The episode's season's number
|
/// The episode's season's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The episode's number
|
/// The episode's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub episode_number: EpisodeNumber,
|
pub episode_number: EpisodeNumber,
|
||||||
/// The number of additional contained episodes
|
/// The number of additional contained episodes.
|
||||||
pub count: u8,
|
pub count: u8,
|
||||||
/// The episode's library
|
/// The episode's library.
|
||||||
pub library_id: LibraryId,
|
pub library_id: LibraryId,
|
||||||
/// The episode's directory
|
/// The episode's directory.
|
||||||
pub directory: PathBytes,
|
pub directory: PathBytes,
|
||||||
/// The episode's media path
|
/// The episode's media path.
|
||||||
pub relative_media_path: String,
|
pub relative_media_path: String,
|
||||||
/// The episode's poster path
|
/// The episode's poster path.
|
||||||
pub relative_poster_path: Option<String>,
|
pub relative_poster_path: Option<String>,
|
||||||
|
|
||||||
/// This episode's show
|
/// This episode's show.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "show_id",
|
from = "show_id",
|
||||||
@@ -368,7 +373,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub show: HasOne<super::shows::Entity>,
|
pub show: HasOne<super::shows::Entity>,
|
||||||
/// This episode's season
|
/// This episode's season.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "(show_id, season_number)",
|
from = "(show_id, season_number)",
|
||||||
@@ -377,7 +382,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub season: HasOne<super::seasons::Entity>,
|
pub season: HasOne<super::seasons::Entity>,
|
||||||
/// The library this episode belongs to
|
/// The library this episode belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "library_id",
|
from = "library_id",
|
||||||
@@ -386,7 +391,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub library: HasOne<super::libraries::Entity>,
|
pub library: HasOne<super::libraries::Entity>,
|
||||||
/// The info for this episode
|
/// The info for this episode.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -397,7 +402,7 @@ pub mod episodes {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::episodes::Entity>,
|
pub info: HasOne<entity::info::episodes::Entity>,
|
||||||
|
|
||||||
/// The watched info for this episode
|
/// The watched info for this episode.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
has_many,
|
has_many,
|
||||||
relation_enum = "Watched",
|
relation_enum = "Watched",
|
||||||
@@ -407,14 +412,16 @@ pub mod episodes {
|
|||||||
pub watched: HasMany<entity::watched::episodes::Entity>,
|
pub watched: HasMany<entity::watched::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macros for creating content entities
|
/// Macros for creating content entities.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
macro_rules! make_content_library {
|
macro_rules! make_content_library {
|
||||||
($db:expr, $id:expr) => {
|
($db:expr, $id:expr) => {
|
||||||
|
drop(
|
||||||
$crate::entity::content::libraries::ActiveModel {
|
$crate::entity::content::libraries::ActiveModel {
|
||||||
id: Set(::flix_model::id::LibraryId::from_raw($id)),
|
id: Set(::flix_model::id::LibraryId::from_raw($id)),
|
||||||
directory: Set(::std::path::PathBuf::new().into()),
|
directory: Set(::std::path::PathBuf::new().into()),
|
||||||
@@ -423,7 +430,8 @@ pub mod test {
|
|||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_content_library;
|
pub(crate) use make_content_library;
|
||||||
@@ -431,6 +439,7 @@ pub mod test {
|
|||||||
macro_rules! make_content_collection {
|
macro_rules! make_content_collection {
|
||||||
($db:expr, $lid:expr, $id:expr, $pid:expr) => {
|
($db:expr, $lid:expr, $id:expr, $pid:expr) => {
|
||||||
$crate::entity::info::test::make_info_collection!($db, $id);
|
$crate::entity::info::test::make_info_collection!($db, $id);
|
||||||
|
drop(
|
||||||
$crate::entity::content::collections::ActiveModel {
|
$crate::entity::content::collections::ActiveModel {
|
||||||
id: Set(::flix_model::id::CollectionId::from_raw($id)),
|
id: Set(::flix_model::id::CollectionId::from_raw($id)),
|
||||||
parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
|
parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
|
||||||
@@ -440,7 +449,8 @@ pub mod test {
|
|||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_content_collection;
|
pub(crate) use make_content_collection;
|
||||||
@@ -448,17 +458,19 @@ pub mod test {
|
|||||||
macro_rules! make_content_movie {
|
macro_rules! make_content_movie {
|
||||||
($db:expr, $lid:expr, $id:expr, $pid:expr) => {
|
($db:expr, $lid:expr, $id:expr, $pid:expr) => {
|
||||||
$crate::entity::info::test::make_info_movie!($db, $id);
|
$crate::entity::info::test::make_info_movie!($db, $id);
|
||||||
|
drop(
|
||||||
$crate::entity::content::movies::ActiveModel {
|
$crate::entity::content::movies::ActiveModel {
|
||||||
id: Set(::flix_model::id::MovieId::from_raw($id)),
|
id: Set(::flix_model::id::MovieId::from_raw($id)),
|
||||||
parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
|
parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
|
||||||
library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
|
library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
|
||||||
directory: Set(::std::path::PathBuf::new().into()),
|
directory: Set(::std::path::PathBuf::new().into()),
|
||||||
relative_media_path: Set(::std::string::String::new()),
|
relative_media_path: Set(::alloc::string::String::new()),
|
||||||
relative_poster_path: Set(::core::option::Option::None),
|
relative_poster_path: Set(::core::option::Option::None),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_content_movie;
|
pub(crate) use make_content_movie;
|
||||||
@@ -466,6 +478,7 @@ pub mod test {
|
|||||||
macro_rules! make_content_show {
|
macro_rules! make_content_show {
|
||||||
($db:expr, $lid:expr, $id:expr, $pid:expr) => {
|
($db:expr, $lid:expr, $id:expr, $pid:expr) => {
|
||||||
$crate::entity::info::test::make_info_show!($db, $id);
|
$crate::entity::info::test::make_info_show!($db, $id);
|
||||||
|
drop(
|
||||||
$crate::entity::content::shows::ActiveModel {
|
$crate::entity::content::shows::ActiveModel {
|
||||||
id: Set(::flix_model::id::ShowId::from_raw($id)),
|
id: Set(::flix_model::id::ShowId::from_raw($id)),
|
||||||
parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
|
parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
|
||||||
@@ -475,7 +488,8 @@ pub mod test {
|
|||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_content_show;
|
pub(crate) use make_content_show;
|
||||||
@@ -483,6 +497,7 @@ pub mod test {
|
|||||||
macro_rules! make_content_season {
|
macro_rules! make_content_season {
|
||||||
($db:expr, $lid:expr, $show:expr, $season:expr) => {
|
($db:expr, $lid:expr, $show:expr, $season:expr) => {
|
||||||
$crate::entity::info::test::make_info_season!($db, $show, $season);
|
$crate::entity::info::test::make_info_season!($db, $show, $season);
|
||||||
|
drop(
|
||||||
$crate::entity::content::seasons::ActiveModel {
|
$crate::entity::content::seasons::ActiveModel {
|
||||||
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
||||||
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
@@ -492,7 +507,8 @@ pub mod test {
|
|||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_content_season;
|
pub(crate) use make_content_season;
|
||||||
@@ -506,6 +522,7 @@ pub mod test {
|
|||||||
};
|
};
|
||||||
(@make, $db:expr, $lid:expr, $show:expr, $season:expr, $episode:expr, $count:literal) => {
|
(@make, $db:expr, $lid:expr, $show:expr, $season:expr, $episode:expr, $count:literal) => {
|
||||||
$crate::entity::info::test::make_info_episode!($db, $show, $season, $episode);
|
$crate::entity::info::test::make_info_episode!($db, $show, $season, $episode);
|
||||||
|
drop(
|
||||||
$crate::entity::content::episodes::ActiveModel {
|
$crate::entity::content::episodes::ActiveModel {
|
||||||
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
||||||
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
@@ -513,12 +530,13 @@ pub mod test {
|
|||||||
count: Set($count),
|
count: Set($count),
|
||||||
library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
|
library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
|
||||||
directory: Set(::std::path::PathBuf::new().into()),
|
directory: Set(::std::path::PathBuf::new().into()),
|
||||||
relative_media_path: Set(::std::string::String::new()),
|
relative_media_path: Set(::alloc::string::String::new()),
|
||||||
relative_poster_path: Set(::core::option::Option::None),
|
relative_poster_path: Set(::core::option::Option::None),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_content_episode;
|
pub(crate) use make_content_episode;
|
||||||
@@ -550,7 +568,10 @@ mod tests {
|
|||||||
use super::super::tests::get_error_kind;
|
use super::super::tests::get_error_kind;
|
||||||
use super::super::tests::{noneable, notsettable};
|
use super::super::tests::{noneable, notsettable};
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
async fn use_test_macros() {
|
async fn use_test_macros() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
@@ -562,8 +583,10 @@ mod tests {
|
|||||||
make_content_episode!(&db, 1, 1, 1, 1);
|
make_content_episode!(&db, 1, 1, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_libraries() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn round_trip_libraries() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_library {
|
macro_rules! assert_library {
|
||||||
@@ -601,8 +624,11 @@ mod tests {
|
|||||||
assert_library!(&db, 6, Success; last_scan_duration);
|
assert_library!(&db, 6, Success; last_scan_duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_collections() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_collections() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_collection {
|
macro_rules! assert_collection {
|
||||||
@@ -656,8 +682,11 @@ mod tests {
|
|||||||
assert_collection!(&db, 7, None, 1, Success; relative_poster_path);
|
assert_collection!(&db, 7, None, 1, Success; relative_poster_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_movies() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_movies() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_movie {
|
macro_rules! assert_movie {
|
||||||
@@ -718,8 +747,11 @@ mod tests {
|
|||||||
assert_movie!(&db, 8, None, 1, Success; relative_poster_path);
|
assert_movie!(&db, 8, None, 1, Success; relative_poster_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_shows() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_shows() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_show {
|
macro_rules! assert_show {
|
||||||
@@ -776,8 +808,11 @@ mod tests {
|
|||||||
assert_show!(&db, 7, None, 1, Success; relative_poster_path);
|
assert_show!(&db, 7, None, 1, Success; relative_poster_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_seasons() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_seasons() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_season {
|
macro_rules! assert_season {
|
||||||
@@ -828,8 +863,11 @@ mod tests {
|
|||||||
assert_season!(&db, 1, 7, 1, Success; relative_poster_path);
|
assert_season!(&db, 1, 7, 1, Success; relative_poster_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_episodes() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_episodes() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_episode {
|
macro_rules! assert_episode {
|
||||||
|
|||||||
+111
-81
@@ -1,7 +1,7 @@
|
|||||||
//! This module contains entities for storing media information such as
|
//! This module contains entities for storing media information such as
|
||||||
//! titles and overviews
|
//! titles and overviews.
|
||||||
|
|
||||||
/// Collection entity
|
/// Collection entity.
|
||||||
pub mod collections {
|
pub mod collections {
|
||||||
use flix_model::id::CollectionId;
|
use flix_model::id::CollectionId;
|
||||||
|
|
||||||
@@ -9,38 +9,39 @@ pub mod collections {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a flix collection
|
/// The database representation of a flix collection.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_info_collections")]
|
#[sea_orm(table_name = "flix_info_collections")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The collection's ID
|
/// The collection's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: CollectionId,
|
pub id: CollectionId,
|
||||||
/// The collection's title
|
/// The collection's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The collection's overview
|
/// The collection's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
|
|
||||||
/// The sortable title
|
/// The sortable title.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub sort_title: String,
|
pub sort_title: String,
|
||||||
/// The filesystem-safe slug
|
/// The filesystem-safe slug.
|
||||||
#[sea_orm(indexed, unique)]
|
#[sea_orm(indexed, unique)]
|
||||||
pub fs_slug: String,
|
pub fs_slug: String,
|
||||||
/// The url-safe slug
|
/// The url-safe slug.
|
||||||
#[sea_orm(indexed, unique)]
|
#[sea_orm(indexed, unique)]
|
||||||
pub web_slug: String,
|
pub web_slug: String,
|
||||||
|
|
||||||
/// Potential content for this collection
|
/// Potential content for this collection.
|
||||||
#[sea_orm(has_one)]
|
#[sea_orm(has_one)]
|
||||||
pub content: HasOne<entity::content::collections::Entity>,
|
pub content: HasOne<entity::content::collections::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Movie entity
|
/// Movie entity.
|
||||||
pub mod movies {
|
pub mod movies {
|
||||||
use flix_model::id::MovieId;
|
use flix_model::id::MovieId;
|
||||||
|
|
||||||
@@ -49,43 +50,44 @@ pub mod movies {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a flix movie
|
/// The database representation of a flix movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_info_movies")]
|
#[sea_orm(table_name = "flix_info_movies")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The movie's ID
|
/// The movie's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: MovieId,
|
pub id: MovieId,
|
||||||
/// The movie's title
|
/// The movie's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The movie's tagline
|
/// The movie's tagline.
|
||||||
pub tagline: String,
|
pub tagline: String,
|
||||||
/// The movie's overview
|
/// The movie's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The movie's release date
|
/// The movie's release date.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
|
||||||
/// The sortable title
|
/// The sortable title.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub sort_title: String,
|
pub sort_title: String,
|
||||||
/// The filesystem-safe slug
|
/// The filesystem-safe slug.
|
||||||
#[sea_orm(indexed, unique)]
|
#[sea_orm(indexed, unique)]
|
||||||
pub fs_slug: String,
|
pub fs_slug: String,
|
||||||
/// The url-safe slug
|
/// The url-safe slug.
|
||||||
#[sea_orm(indexed, unique)]
|
#[sea_orm(indexed, unique)]
|
||||||
pub web_slug: String,
|
pub web_slug: String,
|
||||||
|
|
||||||
/// Potential content for this movie
|
/// Potential content for this movie.
|
||||||
#[sea_orm(has_one)]
|
#[sea_orm(has_one)]
|
||||||
pub content: HasOne<entity::content::movies::Entity>,
|
pub content: HasOne<entity::content::movies::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show entity
|
/// Show entity.
|
||||||
pub mod shows {
|
pub mod shows {
|
||||||
use flix_model::id::ShowId;
|
use flix_model::id::ShowId;
|
||||||
|
|
||||||
@@ -94,50 +96,51 @@ pub mod shows {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a flix show
|
/// The database representation of a flix show.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_info_shows")]
|
#[sea_orm(table_name = "flix_info_shows")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The show's ID
|
/// The show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: ShowId,
|
pub id: ShowId,
|
||||||
/// The show's title
|
/// The show's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The show's tagline
|
/// The show's tagline.
|
||||||
pub tagline: String,
|
pub tagline: String,
|
||||||
/// The show's overview
|
/// The show's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The show's air date
|
/// The show's air date.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
|
||||||
/// The sortable title
|
/// The sortable title.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub sort_title: String,
|
pub sort_title: String,
|
||||||
/// The filesystem-safe slug
|
/// The filesystem-safe slug.
|
||||||
#[sea_orm(indexed, unique)]
|
#[sea_orm(indexed, unique)]
|
||||||
pub fs_slug: String,
|
pub fs_slug: String,
|
||||||
/// The url-safe slug
|
/// The url-safe slug.
|
||||||
#[sea_orm(indexed, unique)]
|
#[sea_orm(indexed, unique)]
|
||||||
pub web_slug: String,
|
pub web_slug: String,
|
||||||
|
|
||||||
/// Seasons that are part of this show
|
/// Seasons that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub seasons: HasMany<super::seasons::Entity>,
|
pub seasons: HasMany<super::seasons::Entity>,
|
||||||
/// Episodes that are part of this show
|
/// Episodes that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
|
|
||||||
/// Potential content for this show
|
/// Potential content for this show.
|
||||||
#[sea_orm(has_one)]
|
#[sea_orm(has_one)]
|
||||||
pub content: HasOne<entity::content::shows::Entity>,
|
pub content: HasOne<entity::content::shows::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Season entity
|
/// Season entity.
|
||||||
pub mod seasons {
|
pub mod seasons {
|
||||||
use flix_model::id::ShowId;
|
use flix_model::id::ShowId;
|
||||||
use flix_model::numbers::SeasonNumber;
|
use flix_model::numbers::SeasonNumber;
|
||||||
@@ -147,26 +150,26 @@ pub mod seasons {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a flix season
|
/// The database representation of a flix season.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_info_seasons")]
|
#[sea_orm(table_name = "flix_info_seasons")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The season's show's ID
|
/// The season's show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub show_id: ShowId,
|
pub show_id: ShowId,
|
||||||
/// The season's number
|
/// The season's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The season's title
|
/// The season's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The season's overview
|
/// The season's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The season's air date
|
/// The season's air date.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
|
||||||
/// The show this season belongs to
|
/// The show this season belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "show_id",
|
from = "show_id",
|
||||||
@@ -175,19 +178,20 @@ pub mod seasons {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub show: HasOne<super::shows::Entity>,
|
pub show: HasOne<super::shows::Entity>,
|
||||||
/// Episodes that are part of this season
|
/// Episodes that are part of this season.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
|
|
||||||
/// Potential content for this season
|
/// Potential content for this season.
|
||||||
#[sea_orm(has_one)]
|
#[sea_orm(has_one)]
|
||||||
pub content: HasOne<entity::content::seasons::Entity>,
|
pub content: HasOne<entity::content::seasons::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Episode entity
|
/// Episode entity.
|
||||||
pub mod episodes {
|
pub mod episodes {
|
||||||
use flix_model::id::ShowId;
|
use flix_model::id::ShowId;
|
||||||
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
@@ -197,29 +201,29 @@ pub mod episodes {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a flix episode
|
/// The database representation of a flix episode.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_info_episodes")]
|
#[sea_orm(table_name = "flix_info_episodes")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The episode's show's ID
|
/// The episode's show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub show_id: ShowId,
|
pub show_id: ShowId,
|
||||||
/// The episode's season's number
|
/// The episode's season's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The episode's number
|
/// The episode's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub episode_number: EpisodeNumber,
|
pub episode_number: EpisodeNumber,
|
||||||
/// The episode's title
|
/// The episode's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The episode's overview
|
/// The episode's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The episode's air date
|
/// The episode's air date.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
|
||||||
/// The show this episode belongs to
|
/// The show this episode belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "show_id",
|
from = "show_id",
|
||||||
@@ -228,7 +232,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub show: HasOne<super::shows::Entity>,
|
pub show: HasOne<super::shows::Entity>,
|
||||||
/// The season this episode belongs to
|
/// The season this episode belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "(show_id, season_number)",
|
from = "(show_id, season_number)",
|
||||||
@@ -238,101 +242,112 @@ pub mod episodes {
|
|||||||
)]
|
)]
|
||||||
pub season: HasOne<super::seasons::Entity>,
|
pub season: HasOne<super::seasons::Entity>,
|
||||||
|
|
||||||
/// Potential content for this episode
|
/// Potential content for this episode.
|
||||||
#[sea_orm(has_one)]
|
#[sea_orm(has_one)]
|
||||||
pub content: HasOne<entity::content::episodes::Entity>,
|
pub content: HasOne<entity::content::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macros for creating info entities
|
/// Macros for creating info entities.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
macro_rules! make_info_collection {
|
macro_rules! make_info_collection {
|
||||||
($db:expr, $id:expr) => {
|
($db:expr, $id:expr) => {
|
||||||
|
drop(
|
||||||
$crate::entity::info::collections::ActiveModel {
|
$crate::entity::info::collections::ActiveModel {
|
||||||
id: Set(::flix_model::id::CollectionId::from_raw($id)),
|
id: Set(::flix_model::id::CollectionId::from_raw($id)),
|
||||||
title: Set(::std::string::String::new()),
|
title: Set(::alloc::string::String::new()),
|
||||||
overview: Set(::std::string::String::new()),
|
overview: Set(::alloc::string::String::new()),
|
||||||
sort_title: Set(::std::string::String::new()),
|
sort_title: Set(::alloc::string::String::new()),
|
||||||
fs_slug: Set(format!("C FS {}", $id)),
|
fs_slug: Set(format!("C FS {}", $id)),
|
||||||
web_slug: Set(format!("C Web {}", $id)),
|
web_slug: Set(format!("C Web {}", $id)),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_info_collection;
|
pub(crate) use make_info_collection;
|
||||||
|
|
||||||
macro_rules! make_info_movie {
|
macro_rules! make_info_movie {
|
||||||
($db:expr, $id:expr) => {
|
($db:expr, $id:expr) => {
|
||||||
|
drop(
|
||||||
$crate::entity::info::movies::ActiveModel {
|
$crate::entity::info::movies::ActiveModel {
|
||||||
id: Set(::flix_model::id::MovieId::from_raw($id)),
|
id: Set(::flix_model::id::MovieId::from_raw($id)),
|
||||||
title: Set(::std::string::String::new()),
|
title: Set(::alloc::string::String::new()),
|
||||||
tagline: Set(::std::string::String::new()),
|
tagline: Set(::alloc::string::String::new()),
|
||||||
overview: Set(::std::string::String::new()),
|
overview: Set(::alloc::string::String::new()),
|
||||||
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
||||||
sort_title: Set(::std::string::String::new()),
|
sort_title: Set(::alloc::string::String::new()),
|
||||||
fs_slug: Set(format!("M FS {}", $id)),
|
fs_slug: Set(format!("M FS {}", $id)),
|
||||||
web_slug: Set(format!("M Web {}", $id)),
|
web_slug: Set(format!("M Web {}", $id)),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_info_movie;
|
pub(crate) use make_info_movie;
|
||||||
|
|
||||||
macro_rules! make_info_show {
|
macro_rules! make_info_show {
|
||||||
($db:expr, $id:expr) => {
|
($db:expr, $id:expr) => {
|
||||||
|
drop(
|
||||||
$crate::entity::info::shows::ActiveModel {
|
$crate::entity::info::shows::ActiveModel {
|
||||||
id: Set(::flix_model::id::ShowId::from_raw($id)),
|
id: Set(::flix_model::id::ShowId::from_raw($id)),
|
||||||
title: Set(::std::string::String::new()),
|
title: Set(::alloc::string::String::new()),
|
||||||
tagline: Set(::std::string::String::new()),
|
tagline: Set(::alloc::string::String::new()),
|
||||||
overview: Set(::std::string::String::new()),
|
overview: Set(::alloc::string::String::new()),
|
||||||
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
||||||
sort_title: Set(::std::string::String::new()),
|
sort_title: Set(::alloc::string::String::new()),
|
||||||
fs_slug: Set(format!("S FS {}", $id)),
|
fs_slug: Set(format!("S FS {}", $id)),
|
||||||
web_slug: Set(format!("S Web {}", $id)),
|
web_slug: Set(format!("S Web {}", $id)),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_info_show;
|
pub(crate) use make_info_show;
|
||||||
|
|
||||||
macro_rules! make_info_season {
|
macro_rules! make_info_season {
|
||||||
($db:expr, $show:expr, $season:expr) => {
|
($db:expr, $show:expr, $season:expr) => {
|
||||||
|
drop(
|
||||||
$crate::entity::info::seasons::ActiveModel {
|
$crate::entity::info::seasons::ActiveModel {
|
||||||
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
||||||
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
title: Set(::std::string::String::new()),
|
title: Set(::alloc::string::String::new()),
|
||||||
overview: Set(::std::string::String::new()),
|
overview: Set(::alloc::string::String::new()),
|
||||||
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_info_season;
|
pub(crate) use make_info_season;
|
||||||
|
|
||||||
macro_rules! make_info_episode {
|
macro_rules! make_info_episode {
|
||||||
($db:expr, $show:expr, $season:expr, $episode:expr) => {
|
($db:expr, $show:expr, $season:expr, $episode:expr) => {
|
||||||
|
drop(
|
||||||
$crate::entity::info::episodes::ActiveModel {
|
$crate::entity::info::episodes::ActiveModel {
|
||||||
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
||||||
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
|
episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
|
||||||
title: Set(::std::string::String::new()),
|
title: Set(::alloc::string::String::new()),
|
||||||
overview: Set(::std::string::String::new()),
|
overview: Set(::alloc::string::String::new()),
|
||||||
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
|
||||||
}
|
}
|
||||||
.insert($db)
|
.insert($db)
|
||||||
.await
|
.await
|
||||||
.expect("insert");
|
.expect("insert"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use make_info_episode;
|
pub(crate) use make_info_episode;
|
||||||
@@ -355,7 +370,10 @@ mod tests {
|
|||||||
make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
|
make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
async fn use_test_macros() {
|
async fn use_test_macros() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
@@ -366,8 +384,10 @@ mod tests {
|
|||||||
make_info_episode!(&db, 1, 1, 1);
|
make_info_episode!(&db, 1, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_collections() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn round_trip_collections() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_collection {
|
macro_rules! assert_collection {
|
||||||
@@ -409,8 +429,10 @@ mod tests {
|
|||||||
assert_collection!(&db, 8, NotNullViolation; web_slug);
|
assert_collection!(&db, 8, NotNullViolation; web_slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_movies() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn round_trip_movies() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_movie {
|
macro_rules! assert_movie {
|
||||||
@@ -458,8 +480,10 @@ mod tests {
|
|||||||
assert_movie!(&db, 10, NotNullViolation; web_slug);
|
assert_movie!(&db, 10, NotNullViolation; web_slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_shows() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn round_trip_shows() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_show {
|
macro_rules! assert_show {
|
||||||
@@ -510,8 +534,11 @@ mod tests {
|
|||||||
assert_show!(&db, 10, NotNullViolation; web_slug);
|
assert_show!(&db, 10, NotNullViolation; web_slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_seasons() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_seasons() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_season {
|
macro_rules! assert_season {
|
||||||
@@ -561,8 +588,11 @@ mod tests {
|
|||||||
assert_season!(&db, 1, 7, NotNullViolation; date);
|
assert_season!(&db, 1, 7, NotNullViolation; date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_episodes() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_episodes() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_episode {
|
macro_rules! assert_episode {
|
||||||
|
|||||||
+107
-57
@@ -1,4 +1,10 @@
|
|||||||
//! Entity structs for interacting with the database
|
//! Entity structs for interacting with the database.
|
||||||
|
|
||||||
|
#![expect(clippy::exhaustive_enums, reason = "reflects the database schema")]
|
||||||
|
#![expect(clippy::exhaustive_structs, reason = "reflects the database schema")]
|
||||||
|
#![expect(clippy::derive_partial_eq_without_eq, reason = "#[sea_orm::model]")]
|
||||||
|
#![expect(clippy::impl_trait_in_params, reason = "#[sea_orm::model]")]
|
||||||
|
#![expect(clippy::same_name_method, reason = "#[sea_orm::model]")]
|
||||||
|
|
||||||
pub mod content;
|
pub mod content;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
@@ -13,8 +19,6 @@ mod tests {
|
|||||||
use flix_tmdb::model::id::TmdbRepr as TmdbReprId;
|
use flix_tmdb::model::id::TmdbRepr as TmdbReprId;
|
||||||
|
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::DatabaseConnection;
|
|
||||||
use sea_orm::DbErr;
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sqlx::error::ErrorKind;
|
use sea_orm::sqlx::error::ErrorKind;
|
||||||
use seamantic::model::id::SeaOrmRepr as SeaOrmReprId;
|
use seamantic::model::id::SeaOrmRepr as SeaOrmReprId;
|
||||||
@@ -32,35 +36,36 @@ mod tests {
|
|||||||
use crate::tests::new_initialized_memory_db;
|
use crate::tests::new_initialized_memory_db;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorKindError {
|
pub(crate) enum ErrorKindNot {
|
||||||
NotRuntimeError,
|
Runtime,
|
||||||
NotSqlxError,
|
Sqlx,
|
||||||
NotDatabaseError,
|
Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_error_kind(error: DbErr) -> Result<ErrorKind, ErrorKindError> {
|
/// Extract the error kind from a [`DbErr`].
|
||||||
let runtime_err = match error {
|
///
|
||||||
DbErr::Conn(runtime_err) => runtime_err,
|
/// # Errors
|
||||||
DbErr::Exec(runtime_err) => runtime_err,
|
/// If the [`DbErr`] internals diverge from [`SqlxError::Database`].
|
||||||
DbErr::Query(runtime_err) => runtime_err,
|
pub(crate) fn get_error_kind(error: DbErr) -> Result<ErrorKind, ErrorKindNot> {
|
||||||
_ => return Err(ErrorKindError::NotRuntimeError),
|
let (DbErr::Conn(runtime_err) | DbErr::Exec(runtime_err) | DbErr::Query(runtime_err)) =
|
||||||
|
error
|
||||||
|
else {
|
||||||
|
return Err(ErrorKindNot::Runtime);
|
||||||
};
|
};
|
||||||
|
|
||||||
let sqlx_err = match runtime_err {
|
let RuntimeErr::SqlxError(sqlx_err) = runtime_err else {
|
||||||
sea_orm::RuntimeErr::SqlxError(sqlx_err) => sqlx_err,
|
return Err(ErrorKindNot::Sqlx);
|
||||||
_ => return Err(ErrorKindError::NotSqlxError),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let database_err = match sqlx_err.as_ref() {
|
let SqlxError::Database(database_err) = sqlx_err.as_ref() else {
|
||||||
sea_orm::SqlxError::Database(database_err) => database_err,
|
return Err(ErrorKindNot::Database);
|
||||||
_ => return Err(ErrorKindError::NotDatabaseError),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(database_err.kind())
|
Ok(database_err.kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper macro for writing tests for `ActiveModel` structs where
|
/// Helper macro for writing tests for `ActiveModel` structs where
|
||||||
/// toggling [sea_orm::ActiveValue] is needed
|
/// toggling [`sea_orm::ActiveValue`] is needed.
|
||||||
macro_rules! notsettable {
|
macro_rules! notsettable {
|
||||||
($field:ident, $value:expr $(, $($skip:ident),+)?) => {
|
($field:ident, $value:expr $(, $($skip:ident),+)?) => {
|
||||||
if notsettable!(@skip, $field $(, $($skip),+)?) {
|
if notsettable!(@skip, $field $(, $($skip),+)?) {
|
||||||
@@ -76,7 +81,7 @@ mod tests {
|
|||||||
pub(super) use notsettable;
|
pub(super) use notsettable;
|
||||||
|
|
||||||
/// Helper macro for writing tests for `ActiveModel` structs where
|
/// Helper macro for writing tests for `ActiveModel` structs where
|
||||||
/// toggling [sea_orm::ActiveValue] is needed
|
/// toggling [`sea_orm::ActiveValue`] is needed.
|
||||||
macro_rules! noneable {
|
macro_rules! noneable {
|
||||||
($field:ident, $value:expr $(, $($skip:ident),+)?) => {
|
($field:ident, $value:expr $(, $($skip:ident),+)?) => {
|
||||||
if noneable!(@skip, $field $(, $($skip),+)?) {
|
if noneable!(@skip, $field $(, $($skip),+)?) {
|
||||||
@@ -148,6 +153,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize a database with data to be deleted.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If any database operation fails, since this is only for testing.
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
async fn initialize_deletion_test_database(id: &DbId) -> DatabaseConnection {
|
async fn initialize_deletion_test_database(id: &DbId) -> DatabaseConnection {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
@@ -284,12 +294,14 @@ mod tests {
|
|||||||
db
|
db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_info_collection() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_info_collection() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::info::collections::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
_ = entity::info::collections::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
id.flix.collection,
|
id.flix.collection,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -368,12 +380,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_info_movie() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_info_movie() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::info::movies::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
_ = entity::info::movies::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
id.flix.movie,
|
id.flix.movie,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -452,12 +466,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_info_show() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_info_show() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::info::shows::Entity::delete_by_id(seamantic::model::id::Id::from_raw(id.flix.show))
|
_ = entity::info::shows::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
|
id.flix.show,
|
||||||
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
.await
|
.await
|
||||||
.expect("Entity::delete_by_id");
|
.expect("Entity::delete_by_id");
|
||||||
@@ -534,12 +552,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_info_season() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_info_season() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::info::seasons::Entity::delete_by_id((
|
_ = entity::info::seasons::Entity::delete_by_id((
|
||||||
seamantic::model::id::Id::from_raw(id.flix.show),
|
seamantic::model::id::Id::from_raw(id.flix.show),
|
||||||
SeasonNumber::new(id.flix.season),
|
SeasonNumber::new(id.flix.season),
|
||||||
))
|
))
|
||||||
@@ -619,12 +639,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_info_episodes() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_info_episodes() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::info::episodes::Entity::delete_by_id((
|
_ = entity::info::episodes::Entity::delete_by_id((
|
||||||
seamantic::model::id::Id::from_raw(id.flix.show),
|
seamantic::model::id::Id::from_raw(id.flix.show),
|
||||||
SeasonNumber::new(id.flix.season),
|
SeasonNumber::new(id.flix.season),
|
||||||
EpisodeNumber::new(id.flix.episode),
|
EpisodeNumber::new(id.flix.episode),
|
||||||
@@ -705,13 +727,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[cfg(feature = "tmdb")]
|
#[cfg(feature = "tmdb")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_tmdb_collection() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_tmdb_collection() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::tmdb::collections::Entity::delete_by_id(flix_tmdb::model::id::Id::from_raw(
|
_ = entity::tmdb::collections::Entity::delete_by_id(flix_tmdb::model::id::Id::from_raw(
|
||||||
id.tmdb.collection,
|
id.tmdb.collection,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -790,13 +814,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[cfg(feature = "tmdb")]
|
#[cfg(feature = "tmdb")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_tmdb_movie() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_tmdb_movie() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::tmdb::movies::Entity::delete_by_id(flix_tmdb::model::id::Id::from_raw(
|
_ = entity::tmdb::movies::Entity::delete_by_id(flix_tmdb::model::id::Id::from_raw(
|
||||||
id.tmdb.movie,
|
id.tmdb.movie,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -875,13 +901,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[cfg(feature = "tmdb")]
|
#[cfg(feature = "tmdb")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_tmdb_show() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_tmdb_show() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::tmdb::shows::Entity::delete_by_id(flix_tmdb::model::id::Id::from_raw(id.tmdb.show))
|
_ = entity::tmdb::shows::Entity::delete_by_id(flix_tmdb::model::id::Id::from_raw(
|
||||||
|
id.tmdb.show,
|
||||||
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
.await
|
.await
|
||||||
.expect("Entity::delete_by_id");
|
.expect("Entity::delete_by_id");
|
||||||
@@ -958,13 +988,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[cfg(feature = "tmdb")]
|
#[cfg(feature = "tmdb")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_tmdb_season() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_tmdb_season() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::tmdb::seasons::Entity::delete_by_id((
|
_ = entity::tmdb::seasons::Entity::delete_by_id((
|
||||||
flix_tmdb::model::id::Id::from_raw(id.tmdb.show),
|
flix_tmdb::model::id::Id::from_raw(id.tmdb.show),
|
||||||
SeasonNumber::new(id.tmdb.season),
|
SeasonNumber::new(id.tmdb.season),
|
||||||
))
|
))
|
||||||
@@ -1044,13 +1076,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[cfg(feature = "tmdb")]
|
#[cfg(feature = "tmdb")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_tmdb_episode() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_tmdb_episode() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::tmdb::episodes::Entity::delete_by_id((
|
_ = entity::tmdb::episodes::Entity::delete_by_id((
|
||||||
flix_tmdb::model::id::Id::from_raw(id.tmdb.show),
|
flix_tmdb::model::id::Id::from_raw(id.tmdb.show),
|
||||||
SeasonNumber::new(id.tmdb.season),
|
SeasonNumber::new(id.tmdb.season),
|
||||||
EpisodeNumber::new(id.tmdb.episode),
|
EpisodeNumber::new(id.tmdb.episode),
|
||||||
@@ -1131,12 +1165,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_content_library() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_content_library() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::content::libraries::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
_ = entity::content::libraries::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
id.content.library,
|
id.content.library,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -1215,12 +1251,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_content_collection() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_content_collection() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::content::collections::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
_ = entity::content::collections::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
id.flix.collection,
|
id.flix.collection,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -1299,12 +1337,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_content_movie() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_content_movie() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::content::movies::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
_ = entity::content::movies::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
id.flix.movie,
|
id.flix.movie,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -1383,12 +1423,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_content_show() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_content_show() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::content::shows::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
_ = entity::content::shows::Entity::delete_by_id(seamantic::model::id::Id::from_raw(
|
||||||
id.flix.show,
|
id.flix.show,
|
||||||
))
|
))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
@@ -1467,12 +1509,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_content_season() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_content_season() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::content::seasons::Entity::delete_by_id((
|
_ = entity::content::seasons::Entity::delete_by_id((
|
||||||
seamantic::model::id::Id::from_raw(id.flix.show),
|
seamantic::model::id::Id::from_raw(id.flix.show),
|
||||||
SeasonNumber::new(id.flix.season),
|
SeasonNumber::new(id.flix.season),
|
||||||
))
|
))
|
||||||
@@ -1552,12 +1596,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_content_episode() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_content_episode() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::content::episodes::Entity::delete_by_id((
|
_ = entity::content::episodes::Entity::delete_by_id((
|
||||||
seamantic::model::id::Id::from_raw(id.flix.show),
|
seamantic::model::id::Id::from_raw(id.flix.show),
|
||||||
SeasonNumber::new(id.flix.season),
|
SeasonNumber::new(id.flix.season),
|
||||||
EpisodeNumber::new(id.flix.episode),
|
EpisodeNumber::new(id.flix.episode),
|
||||||
@@ -1638,12 +1684,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_watched_movie() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_watched_movie() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::watched::movies::Entity::delete_by_id((
|
_ = entity::watched::movies::Entity::delete_by_id((
|
||||||
seamantic::model::id::Id::from_raw(id.flix.movie),
|
seamantic::model::id::Id::from_raw(id.flix.movie),
|
||||||
id.watch.user,
|
id.watch.user,
|
||||||
))
|
))
|
||||||
@@ -1723,12 +1771,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_watched_episode() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
async fn delete_watched_episode() {
|
||||||
let id = DbId::default();
|
let id = DbId::default();
|
||||||
let db = initialize_deletion_test_database(&id).await;
|
let db = initialize_deletion_test_database(&id).await;
|
||||||
|
|
||||||
entity::watched::episodes::Entity::delete_by_id((
|
_ = entity::watched::episodes::Entity::delete_by_id((
|
||||||
seamantic::model::id::Id::from_raw(id.flix.show),
|
seamantic::model::id::Id::from_raw(id.flix.show),
|
||||||
SeasonNumber::new(id.flix.season),
|
SeasonNumber::new(id.flix.season),
|
||||||
EpisodeNumber::new(id.flix.episode),
|
EpisodeNumber::new(id.flix.episode),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! This module contains entities for storing dynamic data from TMDB
|
//! This module contains entities for storing dynamic data from TMDB.
|
||||||
|
|
||||||
/// Collection entity
|
/// Collection entity.
|
||||||
pub mod collections {
|
pub mod collections {
|
||||||
use flix_model::id::CollectionId as FlixId;
|
use flix_model::id::CollectionId as FlixId;
|
||||||
use flix_tmdb::model::id::CollectionId;
|
use flix_tmdb::model::id::CollectionId;
|
||||||
@@ -10,23 +10,23 @@ pub mod collections {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a tmdb collection
|
/// The database representation of a tmdb collection.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_tmdb_collections")]
|
#[sea_orm(table_name = "flix_tmdb_collections")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The collection's TMDB ID
|
/// The collection's TMDB ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_id: CollectionId,
|
pub tmdb_id: CollectionId,
|
||||||
/// The collection's ID
|
/// The collection's ID.
|
||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub flix_id: FlixId,
|
pub flix_id: FlixId,
|
||||||
/// The date of the last update
|
/// The date of the last update.
|
||||||
pub last_update: DateTime<Utc>,
|
pub last_update: DateTime<Utc>,
|
||||||
/// The number of movies in the collection
|
/// The number of movies in the collection.
|
||||||
pub movie_count: u16,
|
pub movie_count: u16,
|
||||||
|
|
||||||
/// The info for this collection
|
/// The info for this collection.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "flix_id",
|
from = "flix_id",
|
||||||
@@ -36,15 +36,16 @@ pub mod collections {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::collections::Entity>,
|
pub info: HasOne<entity::info::collections::Entity>,
|
||||||
|
|
||||||
/// Movies that are in this collection
|
/// Movies that are in this collection.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub movies: HasMany<super::movies::Entity>,
|
pub movies: HasMany<super::movies::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Movie entity
|
/// Movie entity.
|
||||||
pub mod movies {
|
pub mod movies {
|
||||||
use flix_model::id::MovieId as FlixId;
|
use flix_model::id::MovieId as FlixId;
|
||||||
use flix_tmdb::model::id::{CollectionId, MovieId};
|
use flix_tmdb::model::id::{CollectionId, MovieId};
|
||||||
@@ -56,26 +57,26 @@ pub mod movies {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a tmdb movie
|
/// The database representation of a tmdb movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_tmdb_movies")]
|
#[sea_orm(table_name = "flix_tmdb_movies")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The movie's TMDB ID
|
/// The movie's TMDB ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_id: MovieId,
|
pub tmdb_id: MovieId,
|
||||||
/// The movie's ID
|
/// The movie's ID.
|
||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub flix_id: FlixId,
|
pub flix_id: FlixId,
|
||||||
/// The date of the last update
|
/// The date of the last update.
|
||||||
pub last_update: DateTime<Utc>,
|
pub last_update: DateTime<Utc>,
|
||||||
/// The movie's runtime in seconds
|
/// The movie's runtime in seconds.
|
||||||
pub runtime: Seconds,
|
pub runtime: Seconds,
|
||||||
/// The TMDB ID of the collection this movie belongs to
|
/// The TMDB ID of the collection this movie belongs to.
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub collection_id: Option<CollectionId>,
|
pub collection_id: Option<CollectionId>,
|
||||||
|
|
||||||
/// The collection this movie belongs to
|
/// The collection this movie belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "collection_id",
|
from = "collection_id",
|
||||||
@@ -84,7 +85,7 @@ pub mod movies {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub collection: HasOne<super::collections::Entity>,
|
pub collection: HasOne<super::collections::Entity>,
|
||||||
/// The info for this movie
|
/// The info for this movie.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "flix_id",
|
from = "flix_id",
|
||||||
@@ -95,10 +96,11 @@ pub mod movies {
|
|||||||
pub info: HasOne<entity::info::movies::Entity>,
|
pub info: HasOne<entity::info::movies::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show entity
|
/// Show entity.
|
||||||
pub mod shows {
|
pub mod shows {
|
||||||
use flix_model::id::ShowId as FlixId;
|
use flix_model::id::ShowId as FlixId;
|
||||||
use flix_tmdb::model::id::ShowId;
|
use flix_tmdb::model::id::ShowId;
|
||||||
@@ -108,23 +110,23 @@ pub mod shows {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a tmdb show
|
/// The database representation of a tmdb show.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_tmdb_shows")]
|
#[sea_orm(table_name = "flix_tmdb_shows")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The show's TMDB ID
|
/// The show's TMDB ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_id: ShowId,
|
pub tmdb_id: ShowId,
|
||||||
/// The show's ID
|
/// The show's ID.
|
||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub flix_id: FlixId,
|
pub flix_id: FlixId,
|
||||||
/// The movie's runtime in seconds
|
/// The movie's runtime in seconds.
|
||||||
pub last_update: DateTime<Utc>,
|
pub last_update: DateTime<Utc>,
|
||||||
/// The number of seasons the show has
|
/// The number of seasons the show has.
|
||||||
pub number_of_seasons: u32,
|
pub number_of_seasons: u32,
|
||||||
|
|
||||||
/// The info for this show
|
/// The info for this show.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "flix_id",
|
from = "flix_id",
|
||||||
@@ -134,18 +136,19 @@ pub mod shows {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::shows::Entity>,
|
pub info: HasOne<entity::info::shows::Entity>,
|
||||||
|
|
||||||
/// Seasons that are part of this show
|
/// Seasons that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub seasons: HasMany<super::seasons::Entity>,
|
pub seasons: HasMany<super::seasons::Entity>,
|
||||||
/// Episodes that are part of this show
|
/// Episodes that are part of this show.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Season entity
|
/// Season entity.
|
||||||
pub mod seasons {
|
pub mod seasons {
|
||||||
use flix_model::id::ShowId as FlixId;
|
use flix_model::id::ShowId as FlixId;
|
||||||
use flix_model::numbers::SeasonNumber;
|
use flix_model::numbers::SeasonNumber;
|
||||||
@@ -156,27 +159,27 @@ pub mod seasons {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a tmdb season
|
/// The database representation of a tmdb season.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_tmdb_seasons")]
|
#[sea_orm(table_name = "flix_tmdb_seasons")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The season's show's TMDB ID
|
/// The season's show's TMDB ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_show: ShowId,
|
pub tmdb_show: ShowId,
|
||||||
/// The season's TMDB season number
|
/// The season's TMDB season number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_season: SeasonNumber,
|
pub tmdb_season: SeasonNumber,
|
||||||
/// The season's show's ID
|
/// The season's show's ID.
|
||||||
#[sea_orm(unique_key = "flix")]
|
#[sea_orm(unique_key = "flix")]
|
||||||
pub flix_show: FlixId,
|
pub flix_show: FlixId,
|
||||||
/// The season's number
|
/// The season's number.
|
||||||
#[sea_orm(unique_key = "flix")]
|
#[sea_orm(unique_key = "flix")]
|
||||||
pub flix_season: SeasonNumber,
|
pub flix_season: SeasonNumber,
|
||||||
/// The date of the last update
|
/// The date of the last update.
|
||||||
pub last_update: DateTime<Utc>,
|
pub last_update: DateTime<Utc>,
|
||||||
|
|
||||||
/// The show this season belongs to
|
/// The show this season belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "tmdb_show",
|
from = "tmdb_show",
|
||||||
@@ -185,7 +188,7 @@ pub mod seasons {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub show: HasOne<super::shows::Entity>,
|
pub show: HasOne<super::shows::Entity>,
|
||||||
/// The info for this season
|
/// The info for this season.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "(flix_show, flix_season)",
|
from = "(flix_show, flix_season)",
|
||||||
@@ -195,15 +198,16 @@ pub mod seasons {
|
|||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::seasons::Entity>,
|
pub info: HasOne<entity::info::seasons::Entity>,
|
||||||
|
|
||||||
/// Episodes that are part of this season
|
/// Episodes that are part of this season.
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub episodes: HasMany<super::episodes::Entity>,
|
pub episodes: HasMany<super::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Season entity
|
/// Season entity.
|
||||||
pub mod episodes {
|
pub mod episodes {
|
||||||
use flix_model::id::ShowId as FlixId;
|
use flix_model::id::ShowId as FlixId;
|
||||||
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
@@ -215,35 +219,35 @@ pub mod episodes {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a tmdb episode
|
/// The database representation of a tmdb episode.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_tmdb_episodes")]
|
#[sea_orm(table_name = "flix_tmdb_episodes")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The episode's show's TMDB ID
|
/// The episode's show's TMDB ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_show: ShowId,
|
pub tmdb_show: ShowId,
|
||||||
/// The episode's season's TMDB season number
|
/// The episode's season's TMDB season number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_season: SeasonNumber,
|
pub tmdb_season: SeasonNumber,
|
||||||
/// The episode's TMDB episode number
|
/// The episode's TMDB episode number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub tmdb_episode: EpisodeNumber,
|
pub tmdb_episode: EpisodeNumber,
|
||||||
/// The episode's show's ID
|
/// The episode's show's ID.
|
||||||
#[sea_orm(unique_key = "flix")]
|
#[sea_orm(unique_key = "flix")]
|
||||||
pub flix_show: FlixId,
|
pub flix_show: FlixId,
|
||||||
/// The episode's season's number
|
/// The episode's season's number.
|
||||||
#[sea_orm(unique_key = "flix")]
|
#[sea_orm(unique_key = "flix")]
|
||||||
pub flix_season: SeasonNumber,
|
pub flix_season: SeasonNumber,
|
||||||
/// The episode's number
|
/// The episode's number.
|
||||||
#[sea_orm(unique_key = "flix")]
|
#[sea_orm(unique_key = "flix")]
|
||||||
pub flix_episode: EpisodeNumber,
|
pub flix_episode: EpisodeNumber,
|
||||||
/// The date of the last update
|
/// The date of the last update.
|
||||||
pub last_update: DateTime<Utc>,
|
pub last_update: DateTime<Utc>,
|
||||||
/// The episode's runtime in seconds
|
/// The episode's runtime in seconds.
|
||||||
pub runtime: Seconds,
|
pub runtime: Seconds,
|
||||||
|
|
||||||
/// The show this episode belongs to
|
/// The show this episode belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "tmdb_show",
|
from = "tmdb_show",
|
||||||
@@ -252,7 +256,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub show: HasOne<super::shows::Entity>,
|
pub show: HasOne<super::shows::Entity>,
|
||||||
/// The season this episode belongs to
|
/// The season this episode belongs to.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "(tmdb_show, tmdb_season)",
|
from = "(tmdb_show, tmdb_season)",
|
||||||
@@ -261,7 +265,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub season: HasOne<super::seasons::Entity>,
|
pub season: HasOne<super::seasons::Entity>,
|
||||||
/// The info for this episode
|
/// The info for this episode.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "(flix_show, flix_season, flix_episode)",
|
from = "(flix_show, flix_season, flix_episode)",
|
||||||
@@ -272,15 +276,16 @@ pub mod episodes {
|
|||||||
pub info: HasOne<entity::info::episodes::Entity>,
|
pub info: HasOne<entity::info::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macros for creating tmdb entities
|
/// Macros for creating tmdb entities.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
macro_rules! make_tmdb_collection {
|
macro_rules! make_tmdb_collection {
|
||||||
($db:expr, $id:expr, $flix_id:expr) => {
|
($db:expr, $id:expr, $flix_id:expr) => {
|
||||||
$crate::entity::tmdb::collections::ActiveModel {
|
_ = $crate::entity::tmdb::collections::ActiveModel {
|
||||||
tmdb_id: Set(::flix_tmdb::model::id::CollectionId::from_raw($id)),
|
tmdb_id: Set(::flix_tmdb::model::id::CollectionId::from_raw($id)),
|
||||||
flix_id: Set(::flix_model::id::CollectionId::from_raw($flix_id)),
|
flix_id: Set(::flix_model::id::CollectionId::from_raw($flix_id)),
|
||||||
last_update: Set(::chrono::Utc::now()),
|
last_update: Set(::chrono::Utc::now()),
|
||||||
@@ -295,7 +300,7 @@ pub mod test {
|
|||||||
|
|
||||||
macro_rules! make_tmdb_movie {
|
macro_rules! make_tmdb_movie {
|
||||||
($db:expr, $id:expr, $flix_id:expr) => {
|
($db:expr, $id:expr, $flix_id:expr) => {
|
||||||
$crate::entity::tmdb::movies::ActiveModel {
|
_ = $crate::entity::tmdb::movies::ActiveModel {
|
||||||
tmdb_id: Set(::flix_tmdb::model::id::MovieId::from_raw($id)),
|
tmdb_id: Set(::flix_tmdb::model::id::MovieId::from_raw($id)),
|
||||||
flix_id: Set(::flix_model::id::MovieId::from_raw($flix_id)),
|
flix_id: Set(::flix_model::id::MovieId::from_raw($flix_id)),
|
||||||
last_update: Set(::chrono::Utc::now()),
|
last_update: Set(::chrono::Utc::now()),
|
||||||
@@ -311,7 +316,7 @@ pub mod test {
|
|||||||
|
|
||||||
macro_rules! make_tmdb_show {
|
macro_rules! make_tmdb_show {
|
||||||
($db:expr, $id:expr, $flix_id:expr) => {
|
($db:expr, $id:expr, $flix_id:expr) => {
|
||||||
$crate::entity::tmdb::shows::ActiveModel {
|
_ = $crate::entity::tmdb::shows::ActiveModel {
|
||||||
tmdb_id: Set(::flix_tmdb::model::id::ShowId::from_raw($id)),
|
tmdb_id: Set(::flix_tmdb::model::id::ShowId::from_raw($id)),
|
||||||
flix_id: Set(::flix_model::id::ShowId::from_raw($flix_id)),
|
flix_id: Set(::flix_model::id::ShowId::from_raw($flix_id)),
|
||||||
last_update: Set(::chrono::Utc::now()),
|
last_update: Set(::chrono::Utc::now()),
|
||||||
@@ -326,7 +331,7 @@ pub mod test {
|
|||||||
|
|
||||||
macro_rules! make_tmdb_season {
|
macro_rules! make_tmdb_season {
|
||||||
($db:expr, $show:expr, $season:expr, $flix_show:expr, $flix_season:expr) => {
|
($db:expr, $show:expr, $season:expr, $flix_show:expr, $flix_season:expr) => {
|
||||||
$crate::entity::tmdb::seasons::ActiveModel {
|
_ = $crate::entity::tmdb::seasons::ActiveModel {
|
||||||
tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
|
tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
|
||||||
tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)),
|
flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)),
|
||||||
@@ -342,7 +347,7 @@ pub mod test {
|
|||||||
|
|
||||||
macro_rules! make_tmdb_episode {
|
macro_rules! make_tmdb_episode {
|
||||||
($db:expr, $show:expr, $season:expr, $episode:expr, $flix_show:expr, $flix_season:expr, $flix_episode:expr) => {
|
($db:expr, $show:expr, $season:expr, $episode:expr, $flix_show:expr, $flix_season:expr, $flix_episode:expr) => {
|
||||||
$crate::entity::tmdb::episodes::ActiveModel {
|
_ = $crate::entity::tmdb::episodes::ActiveModel {
|
||||||
tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
|
tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
|
||||||
tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
tmdb_episode: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
|
tmdb_episode: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
|
||||||
@@ -385,7 +390,10 @@ mod tests {
|
|||||||
make_tmdb_collection, make_tmdb_episode, make_tmdb_movie, make_tmdb_season, make_tmdb_show,
|
make_tmdb_collection, make_tmdb_episode, make_tmdb_movie, make_tmdb_season, make_tmdb_show,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
async fn use_test_macros() {
|
async fn use_test_macros() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
@@ -402,8 +410,11 @@ mod tests {
|
|||||||
make_tmdb_episode!(&db, 1, 1, 1, 1, 1, 1);
|
make_tmdb_episode!(&db, 1, 1, 1, 1, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_collections() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_collections() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_collection {
|
macro_rules! assert_collection {
|
||||||
@@ -449,8 +460,11 @@ mod tests {
|
|||||||
assert_collection!(&db, 6, 6, NotNullViolation; movie_count);
|
assert_collection!(&db, 6, 6, NotNullViolation; movie_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_movies() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_movies() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_movie {
|
macro_rules! assert_movie {
|
||||||
@@ -502,8 +516,11 @@ mod tests {
|
|||||||
assert_movie!(&db, 7, 7, None, ForeignKeyViolation; collection_id);
|
assert_movie!(&db, 7, 7, None, ForeignKeyViolation; collection_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_shows() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_shows() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_show {
|
macro_rules! assert_show {
|
||||||
@@ -552,8 +569,11 @@ mod tests {
|
|||||||
assert_show!(&db, 6, 6, NotNullViolation; number_of_seasons);
|
assert_show!(&db, 6, 6, NotNullViolation; number_of_seasons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_seasons() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_seasons() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_season {
|
macro_rules! assert_season {
|
||||||
@@ -607,8 +627,11 @@ mod tests {
|
|||||||
assert_season!(&db, 1, 7, 1, 7, NotNullViolation; last_update);
|
assert_season!(&db, 1, 7, 1, 7, NotNullViolation; last_update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_episodes() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_episodes() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_episode {
|
macro_rules! assert_episode {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! This module contains entities for storing watched information
|
//! This module contains entities for storing watched information.
|
||||||
|
|
||||||
/// Collection entity
|
/// Collection entity.
|
||||||
pub mod collections {
|
pub mod collections {
|
||||||
use flix_model::id::{CollectionId, RawId};
|
use flix_model::id::{CollectionId, RawId};
|
||||||
|
|
||||||
@@ -9,21 +9,21 @@ pub mod collections {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a watched movie
|
/// The database representation of a watched movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_watched_collections")]
|
#[sea_orm(table_name = "flix_watched_collections")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The collection's ID
|
/// The collection's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: CollectionId,
|
pub id: CollectionId,
|
||||||
/// The user's ID
|
/// The user's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub user_id: RawId,
|
pub user_id: RawId,
|
||||||
/// The date this collection was watched
|
/// The date this collection was watched.
|
||||||
pub watched_date: DateTime<Utc>,
|
pub watched_date: DateTime<Utc>,
|
||||||
|
|
||||||
/// The info for this collection
|
/// The info for this collection.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -33,15 +33,16 @@ pub mod collections {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::collections::Entity>,
|
pub info: HasOne<entity::info::collections::Entity>,
|
||||||
/// The content for this collection
|
/// The content for this collection.
|
||||||
#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
|
#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
|
||||||
pub content: HasOne<entity::content::collections::Entity>,
|
pub content: HasOne<entity::content::collections::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Movie entity
|
/// Movie entity.
|
||||||
pub mod movies {
|
pub mod movies {
|
||||||
use flix_model::id::{MovieId, RawId};
|
use flix_model::id::{MovieId, RawId};
|
||||||
|
|
||||||
@@ -50,21 +51,21 @@ pub mod movies {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a watched movie
|
/// The database representation of a watched movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_watched_movies")]
|
#[sea_orm(table_name = "flix_watched_movies")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The movie's ID
|
/// The movie's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: MovieId,
|
pub id: MovieId,
|
||||||
/// The user's ID
|
/// The user's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub user_id: RawId,
|
pub user_id: RawId,
|
||||||
/// The date this movie was watched
|
/// The date this movie was watched.
|
||||||
pub watched_date: DateTime<Utc>,
|
pub watched_date: DateTime<Utc>,
|
||||||
|
|
||||||
/// The info for this movie
|
/// The info for this movie.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "id",
|
from = "id",
|
||||||
@@ -73,15 +74,16 @@ pub mod movies {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::movies::Entity>,
|
pub info: HasOne<entity::info::movies::Entity>,
|
||||||
/// The content for this movie
|
/// The content for this movie.
|
||||||
#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
|
#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
|
||||||
pub content: HasOne<entity::content::movies::Entity>,
|
pub content: HasOne<entity::content::movies::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show entity
|
/// Show entity.
|
||||||
pub mod shows {
|
pub mod shows {
|
||||||
use flix_model::id::{RawId, ShowId};
|
use flix_model::id::{RawId, ShowId};
|
||||||
|
|
||||||
@@ -90,21 +92,21 @@ pub mod shows {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a watched movie
|
/// The database representation of a watched movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_watched_shows")]
|
#[sea_orm(table_name = "flix_watched_shows")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The show's ID
|
/// The show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: ShowId,
|
pub id: ShowId,
|
||||||
/// The user's ID
|
/// The user's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub user_id: RawId,
|
pub user_id: RawId,
|
||||||
/// The date this show was watched
|
/// The date this show was watched.
|
||||||
pub watched_date: DateTime<Utc>,
|
pub watched_date: DateTime<Utc>,
|
||||||
|
|
||||||
/// The info for this show
|
/// The info for this show.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -114,15 +116,16 @@ pub mod shows {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::shows::Entity>,
|
pub info: HasOne<entity::info::shows::Entity>,
|
||||||
/// The content for this show
|
/// The content for this show.
|
||||||
#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
|
#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
|
||||||
pub content: HasOne<entity::content::shows::Entity>,
|
pub content: HasOne<entity::content::shows::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Season entity
|
/// Season entity.
|
||||||
pub mod seasons {
|
pub mod seasons {
|
||||||
use flix_model::id::{RawId, ShowId};
|
use flix_model::id::{RawId, ShowId};
|
||||||
use flix_model::numbers::SeasonNumber;
|
use flix_model::numbers::SeasonNumber;
|
||||||
@@ -132,24 +135,24 @@ pub mod seasons {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a watched movie
|
/// The database representation of a watched movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_watched_seasons")]
|
#[sea_orm(table_name = "flix_watched_seasons")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The season's show's ID
|
/// The season's show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub show_id: ShowId,
|
pub show_id: ShowId,
|
||||||
/// The season's number
|
/// The season's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The user's ID
|
/// The user's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub user_id: RawId,
|
pub user_id: RawId,
|
||||||
/// The date this season was watched
|
/// The date this season was watched.
|
||||||
pub watched_date: DateTime<Utc>,
|
pub watched_date: DateTime<Utc>,
|
||||||
|
|
||||||
/// The info for this season
|
/// The info for this season.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -159,7 +162,7 @@ pub mod seasons {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::seasons::Entity>,
|
pub info: HasOne<entity::info::seasons::Entity>,
|
||||||
/// The content for this season
|
/// The content for this season.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Content",
|
relation_enum = "Content",
|
||||||
@@ -170,10 +173,11 @@ pub mod seasons {
|
|||||||
pub content: HasOne<entity::content::seasons::Entity>,
|
pub content: HasOne<entity::content::seasons::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Episode entity
|
/// Episode entity.
|
||||||
pub mod episodes {
|
pub mod episodes {
|
||||||
use flix_model::id::{RawId, ShowId};
|
use flix_model::id::{RawId, ShowId};
|
||||||
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
@@ -183,27 +187,27 @@ pub mod episodes {
|
|||||||
|
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
|
|
||||||
/// The database representation of a watched movie
|
/// The database representation of a watched movie.
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Debug, Clone, DeriveEntityModel)]
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "flix_watched_episodes")]
|
#[sea_orm(table_name = "flix_watched_episodes")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
/// The episode's show's ID
|
/// The episode's show's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub show_id: ShowId,
|
pub show_id: ShowId,
|
||||||
/// The episode's season's number
|
/// The episode's season's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The episode's number
|
/// The episode's number.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub episode_number: EpisodeNumber,
|
pub episode_number: EpisodeNumber,
|
||||||
/// The user's ID
|
/// The user's ID.
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub user_id: RawId,
|
pub user_id: RawId,
|
||||||
/// The date this episode was watched
|
/// The date this episode was watched.
|
||||||
pub watched_date: DateTime<Utc>,
|
pub watched_date: DateTime<Utc>,
|
||||||
|
|
||||||
/// The info for this episode
|
/// The info for this episode.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Info",
|
relation_enum = "Info",
|
||||||
@@ -213,7 +217,7 @@ pub mod episodes {
|
|||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub info: HasOne<entity::info::episodes::Entity>,
|
pub info: HasOne<entity::info::episodes::Entity>,
|
||||||
/// The content for this episode
|
/// The content for this episode.
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
relation_enum = "Content",
|
relation_enum = "Content",
|
||||||
@@ -224,15 +228,16 @@ pub mod episodes {
|
|||||||
pub content: HasOne<entity::content::episodes::Entity>,
|
pub content: HasOne<entity::content::episodes::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macros for creating watched entities
|
/// Macros for creating watched entities.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
macro_rules! make_watched_movie {
|
macro_rules! make_watched_movie {
|
||||||
($db:expr, $id:expr, $user:expr) => {
|
($db:expr, $id:expr, $user:expr) => {
|
||||||
$crate::entity::watched::movies::ActiveModel {
|
_ = $crate::entity::watched::movies::ActiveModel {
|
||||||
id: Set(::flix_model::id::MovieId::from_raw($id)),
|
id: Set(::flix_model::id::MovieId::from_raw($id)),
|
||||||
user_id: Set($user),
|
user_id: Set($user),
|
||||||
watched_date: Set(::chrono::Utc::now()),
|
watched_date: Set(::chrono::Utc::now()),
|
||||||
@@ -246,7 +251,7 @@ pub mod test {
|
|||||||
|
|
||||||
macro_rules! make_watched_episode {
|
macro_rules! make_watched_episode {
|
||||||
($db:expr, $show:expr, $season:expr, $episode:expr, $user:expr) => {
|
($db:expr, $show:expr, $season:expr, $episode:expr, $user:expr) => {
|
||||||
$crate::entity::watched::episodes::ActiveModel {
|
_ = $crate::entity::watched::episodes::ActiveModel {
|
||||||
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
show_id: Set(::flix_model::id::ShowId::from_raw($show)),
|
||||||
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
|
||||||
episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
|
episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
|
||||||
@@ -284,7 +289,10 @@ mod tests {
|
|||||||
use super::super::tests::notsettable;
|
use super::super::tests::notsettable;
|
||||||
use super::test::{make_watched_episode, make_watched_movie};
|
use super::test::{make_watched_episode, make_watched_movie};
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
async fn use_test_macros() {
|
async fn use_test_macros() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
@@ -297,8 +305,11 @@ mod tests {
|
|||||||
make_watched_episode!(&db, 1, 1, 1, 1);
|
make_watched_episode!(&db, 1, 1, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_movies() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_movies() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_movie {
|
macro_rules! assert_movie {
|
||||||
@@ -340,8 +351,11 @@ mod tests {
|
|||||||
assert_movie!(&db, 5, 1, NotNullViolation; watched_date);
|
assert_movie!(&db, 5, 1, NotNullViolation; watched_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_round_trip_episodes() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn round_trip_episodes() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_episode {
|
macro_rules! assert_episode {
|
||||||
@@ -394,13 +408,16 @@ mod tests {
|
|||||||
assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; watched_date);
|
assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; watched_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_query_seasons() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn query_seasons() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_season {
|
macro_rules! assert_season {
|
||||||
($db:expr, $show:literal, $season:literal, $uid:literal, Watched) => {
|
($db:expr, $show:literal, $season:literal, $uid:literal, Watched) => {
|
||||||
assert_season!(@find, $db, $show, $season, $uid)
|
_ = assert_season!(@find, $db, $show, $season, $uid)
|
||||||
.ok_or(())
|
.ok_or(())
|
||||||
.expect("is none");
|
.expect("is none");
|
||||||
};
|
};
|
||||||
@@ -462,13 +479,16 @@ mod tests {
|
|||||||
assert_season!(&db, 1, 2, 3, Unwatched);
|
assert_season!(&db, 1, 2, 3, Unwatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_query_shows() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn query_shows() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_show {
|
macro_rules! assert_show {
|
||||||
($db:expr, $show:literal, $uid:literal, Watched) => {
|
($db:expr, $show:literal, $uid:literal, Watched) => {
|
||||||
assert_show!(@find, $db, $show, $uid)
|
_ = assert_show!(@find, $db, $show, $uid)
|
||||||
.ok_or(())
|
.ok_or(())
|
||||||
.expect("is none");
|
.expect("is none");
|
||||||
};
|
};
|
||||||
@@ -517,13 +537,16 @@ mod tests {
|
|||||||
assert_show!(&db, 1, 2, Unwatched);
|
assert_show!(&db, 1, 2, Unwatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_query_collections() {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
#[expect(clippy::default_numeric_fallback, reason = "unit test")]
|
||||||
|
async fn query_collections() {
|
||||||
let db = new_initialized_memory_db().await;
|
let db = new_initialized_memory_db().await;
|
||||||
|
|
||||||
macro_rules! assert_collection {
|
macro_rules! assert_collection {
|
||||||
($db:expr, $id:literal, $uid:literal, Watched) => {
|
($db:expr, $id:literal, $uid:literal, Watched) => {
|
||||||
assert_collection!(@find, $db, $id, $uid)
|
_ = assert_collection!(@find, $db, $id, $uid)
|
||||||
.ok_or(())
|
.ok_or(())
|
||||||
.expect("is none");
|
.expect("is none");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
//! flix-db provides types for storing persistent data about media
|
//! flix-db provides types for storing persistent data about media.
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
pub mod migration;
|
pub mod migration;
|
||||||
@@ -12,7 +15,8 @@ mod tests {
|
|||||||
|
|
||||||
use crate::connection::Connection;
|
use crate::connection::Connection;
|
||||||
|
|
||||||
pub async fn new_initialized_memory_db() -> DatabaseConnection {
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
pub(crate) async fn new_initialized_memory_db() -> DatabaseConnection {
|
||||||
let options = ConnectOptions::new("sqlite::memory:");
|
let options = ConnectOptions::new("sqlite::memory:");
|
||||||
|
|
||||||
let db = Database::connect(options)
|
let db = Database::connect(options)
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
|
//! Custom views for collections.
|
||||||
|
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::sea_query::Table;
|
use sea_orm::sea_query::Table;
|
||||||
use sea_orm::{ConnectionTrait, DbBackend, Statement};
|
use sea_orm::{DbBackend, Statement};
|
||||||
use sea_orm_migration::prelude::*;
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
/// # Errors
|
||||||
|
/// Database operations can fail.
|
||||||
|
pub(crate) async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table("flix_watched_collections").to_owned())
|
.drop_table(Table::drop().table("flix_watched_collections").to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
manager
|
_ = manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_raw(Statement::from_string(
|
.execute_raw(Statement::from_string(
|
||||||
DbBackend::Sqlite,
|
DbBackend::Sqlite,
|
||||||
r#"
|
"
|
||||||
CREATE VIEW flix_watched_collections AS
|
CREATE VIEW flix_watched_collections AS
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
watched_items AS (
|
watched_items AS (
|
||||||
@@ -81,22 +85,24 @@ pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
|||||||
)
|
)
|
||||||
GROUP BY ci.parent_id, wi.user_id
|
GROUP BY ci.parent_id, wi.user_id
|
||||||
;
|
;
|
||||||
"#,
|
",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
/// # Errors
|
||||||
manager
|
/// Database operations can fail.
|
||||||
|
pub(crate) async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
|
_ = manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_raw(Statement::from_string(
|
.execute_raw(Statement::from_string(
|
||||||
DbBackend::Sqlite,
|
DbBackend::Sqlite,
|
||||||
r#"
|
"
|
||||||
DROP VIEW flix_watched_collections
|
DROP VIEW flix_watched_collections
|
||||||
;
|
;
|
||||||
"#,
|
",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ mod collections;
|
|||||||
mod seasons;
|
mod seasons;
|
||||||
mod shows;
|
mod shows;
|
||||||
|
|
||||||
|
/// The migration entry point.
|
||||||
#[derive(DeriveMigrationName)]
|
#[derive(DeriveMigrationName)]
|
||||||
pub(super) struct Migration;
|
pub(super) struct Migration;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
#[expect(
|
||||||
|
elided_lifetimes_in_paths,
|
||||||
|
reason = "async_trait causes lifetimes to be strange"
|
||||||
|
)]
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
seasons::up(manager).await?;
|
seasons::up(manager).await?;
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
|
//! Custom views for seasons.
|
||||||
|
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::sea_query::Table;
|
use sea_orm::sea_query::Table;
|
||||||
use sea_orm::{ConnectionTrait, DbBackend, Statement};
|
use sea_orm::{DbBackend, Statement};
|
||||||
use sea_orm_migration::prelude::*;
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
/// # Errors
|
||||||
|
/// Database operations can fail.
|
||||||
|
pub(crate) async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table("flix_watched_seasons").to_owned())
|
.drop_table(Table::drop().table("flix_watched_seasons").to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
manager
|
_ = manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_raw(Statement::from_string(
|
.execute_raw(Statement::from_string(
|
||||||
DbBackend::Sqlite,
|
DbBackend::Sqlite,
|
||||||
r#"
|
"
|
||||||
CREATE VIEW flix_watched_seasons AS
|
CREATE VIEW flix_watched_seasons AS
|
||||||
SELECT
|
SELECT
|
||||||
w.show_id,
|
w.show_id,
|
||||||
@@ -36,22 +40,24 @@ pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
|||||||
)
|
)
|
||||||
GROUP BY w.show_id, w.season_number, w.user_id
|
GROUP BY w.show_id, w.season_number, w.user_id
|
||||||
;
|
;
|
||||||
"#,
|
",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
/// # Errors
|
||||||
manager
|
/// Database operations can fail.
|
||||||
|
pub(crate) async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
|
_ = manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_raw(Statement::from_string(
|
.execute_raw(Statement::from_string(
|
||||||
DbBackend::Sqlite,
|
DbBackend::Sqlite,
|
||||||
r#"
|
"
|
||||||
DROP VIEW flix_watched_seasons
|
DROP VIEW flix_watched_seasons
|
||||||
;
|
;
|
||||||
"#,
|
",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
|
//! Custom views for shows.
|
||||||
|
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::sea_query::Table;
|
use sea_orm::sea_query::Table;
|
||||||
use sea_orm::{ConnectionTrait, DbBackend, Statement};
|
use sea_orm::{DbBackend, Statement};
|
||||||
use sea_orm_migration::prelude::*;
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
/// # Errors
|
||||||
|
/// Database operations can fail.
|
||||||
|
pub(crate) async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table("flix_watched_shows").to_owned())
|
.drop_table(Table::drop().table("flix_watched_shows").to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
manager
|
_ = manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_raw(Statement::from_string(
|
.execute_raw(Statement::from_string(
|
||||||
DbBackend::Sqlite,
|
DbBackend::Sqlite,
|
||||||
r#"
|
"
|
||||||
CREATE VIEW flix_watched_shows AS
|
CREATE VIEW flix_watched_shows AS
|
||||||
SELECT
|
SELECT
|
||||||
w.show_id as id,
|
w.show_id as id,
|
||||||
@@ -33,22 +37,24 @@ pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
|||||||
)
|
)
|
||||||
GROUP BY w.show_id, w.user_id
|
GROUP BY w.show_id, w.user_id
|
||||||
;
|
;
|
||||||
"#,
|
",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
/// # Errors
|
||||||
manager
|
/// Database operations can fail.
|
||||||
|
pub(crate) async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
|
_ = manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_raw(Statement::from_string(
|
.execute_raw(Statement::from_string(
|
||||||
DbBackend::Sqlite,
|
DbBackend::Sqlite,
|
||||||
r#"
|
"
|
||||||
DROP VIEW flix_watched_shows
|
DROP VIEW flix_watched_shows
|
||||||
;
|
;
|
||||||
"#,
|
",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Migrations for maintaining the database schema
|
//! Migrations for maintaining the database schema.
|
||||||
|
|
||||||
seamantic::migrations! {
|
seamantic::migrations! {
|
||||||
"seaql_migrations_flix";
|
"seaql_migrations_flix";
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix"
|
name = "flix"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "Mechanisms for interacting with flix media"
|
description = "Mechanisms for interacting with flix media"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = []
|
keywords = ["flix"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! flix provides mechanisms for interacting with flix media
|
//! flix provides mechanisms for interacting with flix media.
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix-fs"
|
name = "flix-fs"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "Filesystem scanner for flix media"
|
description = "Filesystem scanner for flix media"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = []
|
keywords = ["flix"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -23,5 +25,8 @@ thiserror = { workspace = true }
|
|||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-stream = { workspace = true, features = ["fs"] }
|
tokio-stream = { workspace = true, features = ["fs"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
+14
-10
@@ -1,39 +1,43 @@
|
|||||||
|
//! Filesystem errors.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
/// The error type for filesystem scanning operations.
|
/// The error type for filesystem scanning operations.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[expect(clippy::error_impl_error, reason = "Error is a good name here")]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to add more variants")]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// fs::read_dir failed
|
/// `fs::read_dir` failed.
|
||||||
#[error("fs::read_dir: {0}")]
|
#[error("fs::read_dir: {0}")]
|
||||||
ReadDir(io::Error),
|
ReadDir(io::Error),
|
||||||
/// fs::read_dir::next_entry failed
|
/// `fs::read_dir::next_entry` failed.
|
||||||
#[error("fs::read_dir::next_entry: {0}")]
|
#[error("fs::read_dir::next_entry: {0}")]
|
||||||
ReadDirEntry(io::Error),
|
ReadDirEntry(io::Error),
|
||||||
/// fs::read_dir::file_type failed
|
/// `fs::read_dir::file_type` failed.
|
||||||
#[error("fs::read_dir::file_type: {0}")]
|
#[error("fs::read_dir::file_type: {0}")]
|
||||||
FileType(io::Error),
|
FileType(io::Error),
|
||||||
|
|
||||||
/// There is an unexpected file in the directory
|
/// There is an unexpected file in the directory.
|
||||||
#[error("unexpected file")]
|
#[error("unexpected file")]
|
||||||
UnexpectedFile,
|
UnexpectedFile,
|
||||||
/// There is an unexpected folder in the directory
|
/// There is an unexpected folder in the directory.
|
||||||
#[error("unexpected folder")]
|
#[error("unexpected folder")]
|
||||||
UnexpectedFolder,
|
UnexpectedFolder,
|
||||||
/// There is an unexpected non-file item in the directory
|
/// There is an unexpected non-file item in the directory.
|
||||||
#[error("unexpected non-file")]
|
#[error("unexpected non-file")]
|
||||||
UnexpectedNonFile,
|
UnexpectedNonFile,
|
||||||
|
|
||||||
/// There are multiple media files in the directory
|
/// There are multiple media files in the directory.
|
||||||
#[error("duplicate media file")]
|
#[error("duplicate media file")]
|
||||||
DuplicateMediaFile,
|
DuplicateMediaFile,
|
||||||
/// There are multiple poster files in the directory
|
/// There are multiple poster files in the directory.
|
||||||
#[error("duplicate poster file")]
|
#[error("duplicate poster file")]
|
||||||
DuplicatePosterFile,
|
DuplicatePosterFile,
|
||||||
|
|
||||||
/// The directory contains incomplete flix media
|
/// The directory contains incomplete flix media.
|
||||||
#[error("incomplete")]
|
#[error("incomplete")]
|
||||||
Incomplete,
|
Incomplete,
|
||||||
/// Some data is inconsistent with the folder structure
|
/// Some data is inconsistent with the folder structure.
|
||||||
#[error("inconsistent")]
|
#[error("inconsistent")]
|
||||||
Inconsistent,
|
Inconsistent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
|
//! Filesystem scan result items.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// An item returned by scanner streams
|
/// An item returned by scanner streams.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_structs, reason = "unlikely to change")]
|
||||||
pub struct Item<T> {
|
pub struct Item<T> {
|
||||||
/// The path of the item
|
/// The path of the item.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
/// The event relating to the item
|
/// The event relating to the item.
|
||||||
pub event: Result<T, Error>,
|
pub event: Result<T, Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Item<T> {
|
impl<T> Item<T> {
|
||||||
/// Helper function for mapping the inner event [Result]
|
/// Helper function for mapping the inner event [Result].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Item<U> {
|
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Item<U> {
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! flix-fs provides filesystem scanners for flix media
|
//! flix-fs provides filesystem scanners for flix media.
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//! Helper macros.
|
||||||
|
|
||||||
|
/// Defines media file extensions.
|
||||||
macro_rules! is_media_extension {
|
macro_rules! is_media_extension {
|
||||||
() => {
|
() => {
|
||||||
Some("mp4" | "mkv")
|
Some("mp4" | "mkv")
|
||||||
@@ -5,6 +8,7 @@ macro_rules! is_media_extension {
|
|||||||
}
|
}
|
||||||
pub(super) use is_media_extension;
|
pub(super) use is_media_extension;
|
||||||
|
|
||||||
|
/// Defines image file expensions.
|
||||||
macro_rules! is_image_extension {
|
macro_rules! is_image_extension {
|
||||||
() => {
|
() => {
|
||||||
Some("png" | "jpg")
|
Some("png" | "jpg")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! The collection scanner will scan a folder and its children
|
//! The collection scanner will scan a folder and its children.
|
||||||
|
|
||||||
use core::pin::Pin;
|
use core::pin::Pin;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
@@ -17,24 +17,27 @@ use crate::scanner::{
|
|||||||
CollectionScan, EpisodeScan, MediaRef, MovieScan, SeasonScan, ShowScan, generic, movie, show,
|
CollectionScan, EpisodeScan, MediaRef, MovieScan, SeasonScan, ShowScan, generic, movie, show,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A collection item
|
/// A collection item.
|
||||||
pub type Item = crate::Item<Scanner>;
|
pub type Item = crate::Item<Scanner>;
|
||||||
|
|
||||||
/// The scanner for collections
|
/// The scanner for collections.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {
|
pub enum Scanner {
|
||||||
/// A scanned collection
|
/// A scanned collection.
|
||||||
Collection(CollectionScan),
|
Collection(CollectionScan),
|
||||||
/// A scanned movie
|
/// A scanned movie.
|
||||||
Movie(MovieScan),
|
Movie(MovieScan),
|
||||||
/// A scanned show
|
/// A scanned show.
|
||||||
Show(ShowScan),
|
Show(ShowScan),
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Season(SeasonScan),
|
Season(SeasonScan),
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Episode(EpisodeScan),
|
Episode(EpisodeScan),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<movie::Scanner> for Scanner {
|
impl From<movie::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: movie::Scanner) -> Self {
|
fn from(value: movie::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
movie::Scanner::Movie(m) => Self::Movie(m),
|
movie::Scanner::Movie(m) => Self::Movie(m),
|
||||||
@@ -43,6 +46,7 @@ impl From<movie::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<show::Scanner> for Scanner {
|
impl From<show::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: show::Scanner) -> Self {
|
fn from(value: show::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
show::Scanner::Show(s) => Self::Show(s),
|
show::Scanner::Show(s) => Self::Show(s),
|
||||||
@@ -53,6 +57,7 @@ impl From<show::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<generic::Scanner> for Scanner {
|
impl From<generic::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: generic::Scanner) -> Self {
|
fn from(value: generic::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
generic::Scanner::Collection(c) => Self::Collection(c),
|
generic::Scanner::Collection(c) => Self::Collection(c),
|
||||||
@@ -65,7 +70,12 @@ impl From<generic::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Scan a folder for a collection
|
/// Scan a folder for a collection.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_collection(
|
pub fn scan_collection(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
parent_ref: Option<MediaRef<CollectionId>>,
|
parent_ref: Option<MediaRef<CollectionId>>,
|
||||||
@@ -151,7 +161,7 @@ impl Scanner {
|
|||||||
for await event in
|
for await event in
|
||||||
generic::Scanner::scan_detect_folder(&subdir, Some(id_ref.clone()))
|
generic::Scanner::scan_detect_folder(&subdir, Some(id_ref.clone()))
|
||||||
{
|
{
|
||||||
yield event.map(|e| e.into());
|
yield event.map(Into::into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! The episode scanner will scan a folder and exit
|
//! The episode scanner will scan a folder and exit.
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -15,17 +15,23 @@ use crate::Error;
|
|||||||
use crate::macros::{is_image_extension, is_media_extension};
|
use crate::macros::{is_image_extension, is_media_extension};
|
||||||
use crate::scanner::{EpisodeScan, MediaRef};
|
use crate::scanner::{EpisodeScan, MediaRef};
|
||||||
|
|
||||||
/// An episode item
|
/// An episode item.
|
||||||
pub type Item = crate::Item<Scanner>;
|
pub type Item = crate::Item<Scanner>;
|
||||||
|
|
||||||
/// The scanner for epispdes
|
/// The scanner for epispdes.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {
|
pub enum Scanner {
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Episode(EpisodeScan),
|
Episode(EpisodeScan),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Scan a folder for an episode
|
/// Scan a folder for an episode.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_episode(
|
pub fn scan_episode(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
show_ref: MediaRef<ShowId>,
|
show_ref: MediaRef<ShowId>,
|
||||||
@@ -62,6 +68,7 @@ impl Scanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[expect(clippy::filetype_is_file, reason = "we only want regular files")]
|
||||||
if !filetype.is_file() {
|
if !filetype.is_file() {
|
||||||
yield Item {
|
yield Item {
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -19,28 +19,32 @@ use crate::scanner::{
|
|||||||
CollectionScan, EpisodeScan, MediaRef, MovieScan, SeasonScan, ShowScan, collection, movie, show,
|
CollectionScan, EpisodeScan, MediaRef, MovieScan, SeasonScan, ShowScan, collection, movie, show,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Regex for detecting a media folder.
|
||||||
static MEDIA_FOLDER_REGEX: OnceLock<Regex> = OnceLock::new();
|
static MEDIA_FOLDER_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||||
|
/// Regex for detecting a season folder.
|
||||||
static SEASON_FOLDER_REGEX: OnceLock<Regex> = OnceLock::new();
|
static SEASON_FOLDER_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||||
|
|
||||||
/// A collection item
|
/// A collection item.
|
||||||
pub type Item = crate::Item<Scanner>;
|
pub type Item = crate::Item<Scanner>;
|
||||||
|
|
||||||
/// The scanner for collections
|
/// The scanner for collections.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {
|
pub enum Scanner {
|
||||||
/// A scanned collection
|
/// A scanned collection.
|
||||||
Collection(CollectionScan),
|
Collection(CollectionScan),
|
||||||
/// A scanned movie
|
/// A scanned movie.
|
||||||
Movie(MovieScan),
|
Movie(MovieScan),
|
||||||
/// A scanned show
|
/// A scanned show.
|
||||||
Show(ShowScan),
|
Show(ShowScan),
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Season(SeasonScan),
|
Season(SeasonScan),
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Episode(EpisodeScan),
|
Episode(EpisodeScan),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<collection::Scanner> for Scanner {
|
impl From<collection::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: collection::Scanner) -> Self {
|
fn from(value: collection::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
collection::Scanner::Collection(c) => Self::Collection(c),
|
collection::Scanner::Collection(c) => Self::Collection(c),
|
||||||
@@ -53,6 +57,7 @@ impl From<collection::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<movie::Scanner> for Scanner {
|
impl From<movie::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: movie::Scanner) -> Self {
|
fn from(value: movie::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
movie::Scanner::Movie(m) => Self::Movie(m),
|
movie::Scanner::Movie(m) => Self::Movie(m),
|
||||||
@@ -61,6 +66,7 @@ impl From<movie::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<show::Scanner> for Scanner {
|
impl From<show::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: show::Scanner) -> Self {
|
fn from(value: show::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
show::Scanner::Show(s) => Self::Show(s),
|
show::Scanner::Show(s) => Self::Show(s),
|
||||||
@@ -71,13 +77,13 @@ impl From<show::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Helper function for stripping allowed numerical prefixes for sorting ("01 - ")
|
/// Helper function for stripping allowed numerical prefixes for sorting ("01 - ").
|
||||||
fn strip_numeric_prefix(original: &str) -> &str {
|
#[inline]
|
||||||
let mut s = original;
|
fn strip_numeric_prefix(string: &str) -> &str {
|
||||||
while let Some('0'..='9') = s.chars().next() {
|
string
|
||||||
s = &s[1..]
|
.trim_start_matches(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])
|
||||||
}
|
.strip_prefix(" - ")
|
||||||
s.strip_prefix(" - ").unwrap_or(original)
|
.unwrap_or(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect the type of a folder and call the correct scanner. Use
|
/// Detect the type of a folder and call the correct scanner. Use
|
||||||
@@ -85,6 +91,13 @@ impl Scanner {
|
|||||||
/// - Collections
|
/// - Collections
|
||||||
/// - Movies
|
/// - Movies
|
||||||
/// - Shows
|
/// - Shows
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If a static regex is invalid.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_detect_folder(
|
pub fn scan_detect_folder(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
parent: Option<MediaRef<CollectionId>>,
|
parent: Option<MediaRef<CollectionId>>,
|
||||||
@@ -95,12 +108,14 @@ impl Scanner {
|
|||||||
Show,
|
Show,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::panic, reason = "static regex string")]
|
||||||
let media_folder_re = MEDIA_FOLDER_REGEX.get_or_init(|| {
|
let media_folder_re = MEDIA_FOLDER_REGEX.get_or_init(|| {
|
||||||
Regex::new(r"^[[[:alnum:]]' -]+ \([[:digit:]]+\)( \[[[:digit:]]+\])?$")
|
Regex::new(r"^[[[:alnum:]]' -]+ \([[:digit:]]+\)( \[[[:digit:]]+\])?$")
|
||||||
.unwrap_or_else(|err| panic!("regex is invalid: {err}"))
|
.unwrap_or_else(|err| panic!("regex is invalid: {err}"))
|
||||||
});
|
});
|
||||||
|
#[expect(clippy::panic, reason = "static regex string")]
|
||||||
let season_folder_re = SEASON_FOLDER_REGEX.get_or_init(|| {
|
let season_folder_re = SEASON_FOLDER_REGEX.get_or_init(|| {
|
||||||
Regex::new(r"^S[[:digit:]]+$").unwrap_or_else(|err| panic!("regex is invalid: {err}"))
|
Regex::new("^S[[:digit:]]+$").unwrap_or_else(|err| panic!("regex is invalid: {err}"))
|
||||||
});
|
});
|
||||||
|
|
||||||
stream!({
|
stream!({
|
||||||
@@ -204,7 +219,7 @@ impl Scanner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for await event in collection::Scanner::scan_collection(path, parent, id) {
|
for await event in collection::Scanner::scan_collection(path, parent, id) {
|
||||||
yield event.map(|e| e.into());
|
yield event.map(Into::into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediaType::Movie => {
|
MediaType::Movie => {
|
||||||
@@ -214,7 +229,7 @@ impl Scanner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for await event in movie::Scanner::scan_movie(path, parent, id) {
|
for await event in movie::Scanner::scan_movie(path, parent, id) {
|
||||||
yield event.map(|e| e.into());
|
yield event.map(Into::into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediaType::Show => {
|
MediaType::Show => {
|
||||||
@@ -224,7 +239,7 @@ impl Scanner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for await event in show::Scanner::scan_show(path, parent, id) {
|
for await event in show::Scanner::scan_show(path, parent, id) {
|
||||||
yield event.map(|e| e.into());
|
yield event.map(Into::into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! The library scanner will scan an entire directory using the generic
|
//! The library scanner will scan an entire directory using the generic
|
||||||
//! scanner
|
//! scanner.
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -12,14 +12,20 @@ use tokio_stream::wrappers::ReadDirStream;
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::scanner::generic;
|
use crate::scanner::generic;
|
||||||
|
|
||||||
/// A library item
|
/// A library item.
|
||||||
pub type Item = crate::Item<generic::Scanner>;
|
pub type Item = crate::Item<generic::Scanner>;
|
||||||
|
|
||||||
/// The scanner for collections
|
/// The scanner for collections.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {}
|
pub enum Scanner {}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Scan a folder for a library
|
/// Scan a folder for a library.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_library(path: &Path) -> impl Stream<Item = Item> {
|
pub fn scan_library(path: &Path) -> impl Stream<Item = Item> {
|
||||||
stream!({
|
stream!({
|
||||||
let dirs = match fs::read_dir(path).await {
|
let dirs = match fs::read_dir(path).await {
|
||||||
@@ -58,7 +64,7 @@ impl Scanner {
|
|||||||
match path.extension().and_then(OsStr::to_str) {
|
match path.extension().and_then(OsStr::to_str) {
|
||||||
Some(_) | None => {
|
Some(_) | None => {
|
||||||
yield Item {
|
yield Item {
|
||||||
path: path.to_owned(),
|
path: path.clone(),
|
||||||
event: Err(Error::UnexpectedFile),
|
event: Err(Error::UnexpectedFile),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! This module contains all of the filesystem scanner modules
|
//! This module contains all of the filesystem scanner modules.
|
||||||
//!
|
//!
|
||||||
//! The most common scanner to use is [generic::Scanner] which will
|
//! The most common scanner to use is [`generic::Scanner`] which will
|
||||||
//! automatically detect and use the appropriate scanner.
|
//! automatically detect and use the appropriate scanner.
|
||||||
|
|
||||||
use flix_model::id::{CollectionId, MovieId, ShowId};
|
use flix_model::id::{CollectionId, MovieId, ShowId};
|
||||||
@@ -18,82 +18,118 @@ pub mod episode;
|
|||||||
pub mod season;
|
pub mod season;
|
||||||
pub mod show;
|
pub mod show;
|
||||||
|
|
||||||
/// A reference to a piece of media
|
/// A reference to a piece of media.
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum MediaRef<ID> {
|
pub enum MediaRef<ID> {
|
||||||
/// An explicit ID
|
/// An explicit ID.
|
||||||
Id(ID),
|
Id(ID),
|
||||||
/// A filesystem slug
|
/// A filesystem slug.
|
||||||
Slug(String),
|
Slug(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ID> MediaRef<ID> {
|
impl<ID> MediaRef<ID> {
|
||||||
/// Get the slug if it exists
|
/// Get the slug if it exists.
|
||||||
|
#[inline]
|
||||||
pub fn into_slug(self) -> Option<String> {
|
pub fn into_slug(self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
MediaRef::Id(_) => None,
|
Self::Id(_) => None,
|
||||||
MediaRef::Slug(slug) => Some(slug),
|
Self::Slug(slug) => Some(slug),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A scanned collection
|
/// A scanned collection.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(
|
||||||
|
clippy::exhaustive_structs,
|
||||||
|
reason = "it will be a breaking change when more information is scanned"
|
||||||
|
)]
|
||||||
pub struct CollectionScan {
|
pub struct CollectionScan {
|
||||||
/// The ID of the parent collection (if any)
|
/// The ID of the parent collection (if any).
|
||||||
pub parent_ref: Option<MediaRef<CollectionId>>,
|
pub parent_ref: Option<MediaRef<CollectionId>>,
|
||||||
/// The ID of the collection
|
/// The ID of the collection.
|
||||||
pub id_ref: MediaRef<CollectionId>,
|
pub id_ref: MediaRef<CollectionId>,
|
||||||
/// The file name of the poster file
|
/// The file name of the poster file.
|
||||||
pub poster_file_name: Option<String>,
|
pub poster_file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A scanned movie
|
/// A scanned movie.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(
|
||||||
|
clippy::exhaustive_structs,
|
||||||
|
reason = "it will be a breaking change when more information is scanned"
|
||||||
|
)]
|
||||||
pub struct MovieScan {
|
pub struct MovieScan {
|
||||||
/// The ID of the parent collection (if any)
|
/// The ID of the parent collection (if any).
|
||||||
pub parent_ref: Option<MediaRef<CollectionId>>,
|
pub parent_ref: Option<MediaRef<CollectionId>>,
|
||||||
/// The ID of the movie
|
/// The ID of the movie.
|
||||||
pub id_ref: MediaRef<MovieId>,
|
pub id_ref: MediaRef<MovieId>,
|
||||||
/// The file name of the media file
|
/// The file name of the media file.
|
||||||
pub media_file_name: String,
|
pub media_file_name: String,
|
||||||
/// The file name of the poster file
|
/// The file name of the poster file.
|
||||||
pub poster_file_name: Option<String>,
|
pub poster_file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A scanned show
|
/// A scanned show.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(
|
||||||
|
clippy::exhaustive_structs,
|
||||||
|
reason = "it will be a breaking change when more information is scanned"
|
||||||
|
)]
|
||||||
pub struct ShowScan {
|
pub struct ShowScan {
|
||||||
/// The ID of the parent collection (if any)
|
/// The ID of the parent collection (if any).
|
||||||
pub parent_ref: Option<MediaRef<CollectionId>>,
|
pub parent_ref: Option<MediaRef<CollectionId>>,
|
||||||
/// The ID of the show
|
/// The ID of the show.
|
||||||
pub id_ref: MediaRef<ShowId>,
|
pub id_ref: MediaRef<ShowId>,
|
||||||
/// The file name of the poster file
|
/// The file name of the poster file.
|
||||||
pub poster_file_name: Option<String>,
|
pub poster_file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A scanned season
|
/// A scanned season.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(
|
||||||
|
clippy::exhaustive_structs,
|
||||||
|
reason = "it will be a breaking change when more information is scanned"
|
||||||
|
)]
|
||||||
pub struct SeasonScan {
|
pub struct SeasonScan {
|
||||||
/// The ID of the show this season belongs to
|
/// The ID of the show this season belongs to.
|
||||||
pub show_ref: MediaRef<ShowId>,
|
pub show_ref: MediaRef<ShowId>,
|
||||||
/// The season this episode belongs to
|
/// The season this episode belongs to.
|
||||||
pub season: SeasonNumber,
|
pub season: SeasonNumber,
|
||||||
/// The file name of the poster file
|
/// The file name of the poster file.
|
||||||
pub poster_file_name: Option<String>,
|
pub poster_file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[expect(
|
||||||
|
clippy::exhaustive_structs,
|
||||||
|
reason = "it will be a breaking change when more information is scanned"
|
||||||
|
)]
|
||||||
pub struct EpisodeScan {
|
pub struct EpisodeScan {
|
||||||
/// The ID of the show this episode belongs to
|
/// The ID of the show this episode belongs to.
|
||||||
pub show_ref: MediaRef<ShowId>,
|
pub show_ref: MediaRef<ShowId>,
|
||||||
/// The season this episode belongs to
|
/// The season this episode belongs to.
|
||||||
pub season: SeasonNumber,
|
pub season: SeasonNumber,
|
||||||
/// The number(s) of this episode
|
/// The number(s) of this episode.
|
||||||
pub episode: EpisodeNumbers,
|
pub episode: EpisodeNumbers,
|
||||||
/// The file name of the media file
|
/// The file name of the media file.
|
||||||
pub media_file_name: String,
|
pub media_file_name: String,
|
||||||
/// The file name of the poster file
|
/// The file name of the poster file.
|
||||||
pub poster_file_name: Option<String>,
|
pub poster_file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{MediaRef, MovieId, library};
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
|
#[test]
|
||||||
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
fn use_fn() {
|
||||||
|
let tmp = tempfile::tempdir().expect("temporary directory exists");
|
||||||
|
drop(library::Scanner::scan_library(tmp.path()));
|
||||||
|
drop(MediaRef::Id(MovieId::from_raw(0)).into_slug());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! The movie scanner will scan a folder and exit
|
//! The movie scanner will scan a folder and exit.
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -14,17 +14,23 @@ use crate::Error;
|
|||||||
use crate::macros::{is_image_extension, is_media_extension};
|
use crate::macros::{is_image_extension, is_media_extension};
|
||||||
use crate::scanner::{MediaRef, MovieScan};
|
use crate::scanner::{MediaRef, MovieScan};
|
||||||
|
|
||||||
/// An movie item
|
/// An movie item.
|
||||||
pub type Item = crate::Item<Scanner>;
|
pub type Item = crate::Item<Scanner>;
|
||||||
|
|
||||||
/// The scanner for movies
|
/// The scanner for movies.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {
|
pub enum Scanner {
|
||||||
/// A scanned movie
|
/// A scanned movie.
|
||||||
Movie(MovieScan),
|
Movie(MovieScan),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Scan a folder for a movie
|
/// Scan a folder for a movie.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_movie(
|
pub fn scan_movie(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
parent_ref: Option<MediaRef<CollectionId>>,
|
parent_ref: Option<MediaRef<CollectionId>>,
|
||||||
@@ -60,6 +66,7 @@ impl Scanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[expect(clippy::filetype_is_file, reason = "we only want regular files")]
|
||||||
if !filetype.is_file() {
|
if !filetype.is_file() {
|
||||||
yield Item {
|
yield Item {
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
//! The episode scanner will scan a folder and its children
|
//! The episode scanner will scan a folder and its children.
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use flix_model::id::ShowId;
|
use flix_model::id::ShowId;
|
||||||
use flix_model::numbers::{EpisodeNumber, EpisodeNumbers, SeasonNumber};
|
use flix_model::numbers::{EpisodeNumbers, SeasonNumber};
|
||||||
|
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
@@ -15,18 +15,21 @@ use crate::Error;
|
|||||||
use crate::macros::is_image_extension;
|
use crate::macros::is_image_extension;
|
||||||
use crate::scanner::{EpisodeScan, MediaRef, SeasonScan, episode};
|
use crate::scanner::{EpisodeScan, MediaRef, SeasonScan, episode};
|
||||||
|
|
||||||
/// A season item
|
/// A season item.
|
||||||
pub type Item = crate::Item<Scanner>;
|
pub type Item = crate::Item<Scanner>;
|
||||||
|
|
||||||
/// The scanner for seasons
|
/// The scanner for seasons.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {
|
pub enum Scanner {
|
||||||
/// A scanned season
|
/// A scanned season.
|
||||||
Season(SeasonScan),
|
Season(SeasonScan),
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Episode(EpisodeScan),
|
Episode(EpisodeScan),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<episode::Scanner> for Scanner {
|
impl From<episode::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: episode::Scanner) -> Self {
|
fn from(value: episode::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
episode::Scanner::Episode(e) => Self::Episode(e),
|
episode::Scanner::Episode(e) => Self::Episode(e),
|
||||||
@@ -35,7 +38,11 @@ impl From<episode::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Scan a folder for a season and its episodes
|
/// Scan a folder for a season and its episodes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_season(
|
pub fn scan_season(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
show_ref: MediaRef<ShowId>,
|
show_ref: MediaRef<ShowId>,
|
||||||
@@ -158,7 +165,7 @@ impl Scanner {
|
|||||||
|
|
||||||
let Ok(episode_numbers) = e_str
|
let Ok(episode_numbers) = e_str
|
||||||
.split('E')
|
.split('E')
|
||||||
.map(|s| s.parse::<EpisodeNumber>())
|
.map(str::parse)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
else {
|
else {
|
||||||
yield Item {
|
yield Item {
|
||||||
@@ -181,7 +188,7 @@ impl Scanner {
|
|||||||
season_number,
|
season_number,
|
||||||
episode_numbers,
|
episode_numbers,
|
||||||
) {
|
) {
|
||||||
yield event.map(|e| e.into());
|
yield event.map(Into::into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! The show scanner will scan a folder and its children
|
//! The show scanner will scan a folder and its children.
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -15,20 +15,23 @@ use crate::Error;
|
|||||||
use crate::macros::is_image_extension;
|
use crate::macros::is_image_extension;
|
||||||
use crate::scanner::{EpisodeScan, MediaRef, SeasonScan, ShowScan, season};
|
use crate::scanner::{EpisodeScan, MediaRef, SeasonScan, ShowScan, season};
|
||||||
|
|
||||||
/// A show item
|
/// A show item.
|
||||||
pub type Item = crate::Item<Scanner>;
|
pub type Item = crate::Item<Scanner>;
|
||||||
|
|
||||||
/// The scanner for shows
|
/// The scanner for shows.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to change")]
|
||||||
pub enum Scanner {
|
pub enum Scanner {
|
||||||
/// A scanned show
|
/// A scanned show.
|
||||||
Show(ShowScan),
|
Show(ShowScan),
|
||||||
/// A scanned season
|
/// A scanned season.
|
||||||
Season(SeasonScan),
|
Season(SeasonScan),
|
||||||
/// A scanned episode
|
/// A scanned episode.
|
||||||
Episode(EpisodeScan),
|
Episode(EpisodeScan),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<season::Scanner> for Scanner {
|
impl From<season::Scanner> for Scanner {
|
||||||
|
#[inline]
|
||||||
fn from(value: season::Scanner) -> Self {
|
fn from(value: season::Scanner) -> Self {
|
||||||
match value {
|
match value {
|
||||||
season::Scanner::Season(s) => Self::Season(s),
|
season::Scanner::Season(s) => Self::Season(s),
|
||||||
@@ -38,7 +41,11 @@ impl From<season::Scanner> for Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
/// Scan a folder for a show and its seasons/episodes
|
/// Scan a folder for a show and its seasons/episodes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::allow_attributes, reason = "for tail_expr_drop_order")]
|
||||||
|
#[allow(tail_expr_drop_order, reason = "bug in stream! macro")]
|
||||||
|
#[expect(clippy::semicolon_if_nothing_returned, reason = "false positive")]
|
||||||
pub fn scan_show(
|
pub fn scan_show(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
parent_ref: Option<MediaRef<CollectionId>>,
|
parent_ref: Option<MediaRef<CollectionId>>,
|
||||||
@@ -143,7 +150,7 @@ impl Scanner {
|
|||||||
for await event in
|
for await event in
|
||||||
season::Scanner::scan_season(&season_dir, id_ref.clone(), season_number)
|
season::Scanner::scan_season(&season_dir, id_ref.clone(), season_number)
|
||||||
{
|
{
|
||||||
yield event.map(|e| e.into());
|
yield event.map(Into::into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix-model"
|
name = "flix-model"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "Core types for flix data"
|
description = "Core types for flix data"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = []
|
keywords = ["flix"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
+29
-6
@@ -1,26 +1,49 @@
|
|||||||
//! This module contains types relating to flix media IDs
|
//! This module contains types relating to flix media IDs.
|
||||||
|
|
||||||
|
#![expect(clippy::module_name_repetitions, reason = "ID types end with Id")]
|
||||||
|
|
||||||
use seamantic::model::id::Id;
|
use seamantic::model::id::Id;
|
||||||
|
|
||||||
/// Type alias for the raw ID representation
|
/// Type alias for the raw ID representation.
|
||||||
pub use seamantic::model::id::SeaOrmRepr as RawId;
|
pub use seamantic::model::id::SeaOrmRepr as RawId;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Library {}
|
pub enum Library {}
|
||||||
/// Type alias for a library ID
|
/// Type alias for a library ID.
|
||||||
pub type LibraryId = Id<Library>;
|
pub type LibraryId = Id<Library>;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Collection {}
|
pub enum Collection {}
|
||||||
/// Type alias for a collection ID
|
/// Type alias for a collection ID.
|
||||||
pub type CollectionId = Id<Collection>;
|
pub type CollectionId = Id<Collection>;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Movie {}
|
pub enum Movie {}
|
||||||
/// Type alias for a movie ID
|
/// Type alias for a movie ID.
|
||||||
pub type MovieId = Id<Movie>;
|
pub type MovieId = Id<Movie>;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Show {}
|
pub enum Show {}
|
||||||
/// Type alias for a show ID
|
/// Type alias for a show ID.
|
||||||
pub type ShowId = Id<Show>;
|
pub type ShowId = Id<Show>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{CollectionId, LibraryId, MovieId, ShowId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
_ = CollectionId::from_raw(0);
|
||||||
|
_ = LibraryId::from_raw(0);
|
||||||
|
_ = MovieId::from_raw(0);
|
||||||
|
_ = ShowId::from_raw(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! flix-model provides core types for flix data
|
//! flix-model provides core types for flix data.
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
|||||||
+60
-26
@@ -1,4 +1,4 @@
|
|||||||
//! This module contains season and episode numbers and related errors
|
//! This module contains season and episode numbers and related errors.
|
||||||
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::ops::RangeInclusive;
|
use core::ops::RangeInclusive;
|
||||||
@@ -7,7 +7,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use seamantic::sea_orm;
|
use seamantic::sea_orm;
|
||||||
|
|
||||||
/// Newtype for representing season numbers
|
/// Newtype for representing season numbers.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
|
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
|
||||||
)]
|
)]
|
||||||
@@ -17,13 +17,16 @@ use seamantic::sea_orm;
|
|||||||
pub struct SeasonNumber(u32);
|
pub struct SeasonNumber(u32);
|
||||||
|
|
||||||
impl SeasonNumber {
|
impl SeasonNumber {
|
||||||
/// Create a `SeasonNumber` from an integer
|
/// Create a [`SeasonNumber`] from an integer.
|
||||||
pub fn new(value: u32) -> Self {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(value: u32) -> Self {
|
||||||
Self(value)
|
Self(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SeasonNumber {
|
impl fmt::Display for SeasonNumber {
|
||||||
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
self.0.fmt(f)
|
self.0.fmt(f)
|
||||||
}
|
}
|
||||||
@@ -32,12 +35,13 @@ impl fmt::Display for SeasonNumber {
|
|||||||
impl FromStr for SeasonNumber {
|
impl FromStr for SeasonNumber {
|
||||||
type Err = <u32 as FromStr>::Err;
|
type Err = <u32 as FromStr>::Err;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
u32::from_str(s).map(Self)
|
u32::from_str(s).map(Self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Newtype for representing episode numbers
|
/// Newtype for representing episode numbers.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
|
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
|
||||||
)]
|
)]
|
||||||
@@ -47,18 +51,23 @@ impl FromStr for SeasonNumber {
|
|||||||
pub struct EpisodeNumber(u32);
|
pub struct EpisodeNumber(u32);
|
||||||
|
|
||||||
impl EpisodeNumber {
|
impl EpisodeNumber {
|
||||||
/// Create an `EpisodeNumber` from an integer
|
/// Create an [`EpisodeNumber`] from an integer.
|
||||||
pub fn new(value: u32) -> Self {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(value: u32) -> Self {
|
||||||
Self(value)
|
Self(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the underlying value
|
/// Get the underlying value.
|
||||||
pub fn into_inner(self) -> u32 {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn into_inner(self) -> u32 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for EpisodeNumber {
|
impl fmt::Display for EpisodeNumber {
|
||||||
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
self.0.fmt(f)
|
self.0.fmt(f)
|
||||||
}
|
}
|
||||||
@@ -67,33 +76,40 @@ impl fmt::Display for EpisodeNumber {
|
|||||||
impl FromStr for EpisodeNumber {
|
impl FromStr for EpisodeNumber {
|
||||||
type Err = <u32 as FromStr>::Err;
|
type Err = <u32 as FromStr>::Err;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
u32::from_str(s).map(Self)
|
u32::from_str(s).map(Self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Potential errors when building EpisodeNumbers
|
/// Potential errors when building [`EpisodeNumbers`].
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||||
pub enum Error {
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to add new variants")]
|
||||||
/// There are no episodes
|
pub enum EpisodeNumbersError {
|
||||||
|
/// There are no episodes.
|
||||||
#[error("zero episodes")]
|
#[error("zero episodes")]
|
||||||
Zero,
|
Zero,
|
||||||
/// There are gaps in the episodes
|
/// There are gaps in the episodes.
|
||||||
#[error("noncontiguous episodes")]
|
#[error("noncontiguous episodes")]
|
||||||
Noncontiguous,
|
Noncontiguous,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper for handling single and multi-episode entries
|
/// A wrapper for handling single and multi-episode entries.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[expect(
|
||||||
|
clippy::module_name_repetitions,
|
||||||
|
reason = "Episodes is not a good name"
|
||||||
|
)]
|
||||||
pub struct EpisodeNumbers(RangeInclusive<EpisodeNumber>);
|
pub struct EpisodeNumbers(RangeInclusive<EpisodeNumber>);
|
||||||
|
|
||||||
impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
||||||
type Error = Error;
|
type Error = EpisodeNumbersError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn try_from(value: &[EpisodeNumber]) -> Result<Self, Self::Error> {
|
fn try_from(value: &[EpisodeNumber]) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
[] => Err(Error::Zero),
|
[] => Err(EpisodeNumbersError::Zero),
|
||||||
[n] => Ok(Self(*n..=*n)),
|
[n] => Ok(Self(*n..=*n)),
|
||||||
_ => {
|
_ => {
|
||||||
// min and max will always exist
|
// min and max will always exist
|
||||||
@@ -102,12 +118,12 @@ impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
|||||||
let len = value.len();
|
let len = value.len();
|
||||||
|
|
||||||
if usize::try_from(max.0.saturating_sub(min.0).saturating_add(1)) != Ok(len) {
|
if usize::try_from(max.0.saturating_sub(min.0).saturating_add(1)) != Ok(len) {
|
||||||
return Err(Error::Noncontiguous);
|
return Err(EpisodeNumbersError::Noncontiguous);
|
||||||
}
|
}
|
||||||
|
|
||||||
let set: HashSet<_> = value.iter().copied().collect();
|
let set: HashSet<_> = value.iter().copied().collect();
|
||||||
if set.len() != len {
|
if set.len() != len {
|
||||||
return Err(Error::Noncontiguous);
|
return Err(EpisodeNumbersError::Noncontiguous);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(min..=max))
|
Ok(Self(min..=max))
|
||||||
@@ -117,27 +133,45 @@ impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EpisodeNumbers {
|
impl EpisodeNumbers {
|
||||||
/// Create an [EpisodeNumbers] from a starting number and a count.
|
/// Create an [`EpisodeNumbers`] from a starting number and a count.
|
||||||
/// `count` should be zero for single episodes.
|
/// `count` should be zero for single episodes.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn new(start: EpisodeNumber, count: u8) -> Self {
|
pub fn new(start: EpisodeNumber, count: u8) -> Self {
|
||||||
Self(start..=EpisodeNumber(start.0.saturating_add(count.into())))
|
Self(start..=EpisodeNumber(start.0.saturating_add(count.into())))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the range of episodes
|
/// Get the range of episodes.
|
||||||
pub fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render this [EpisodeNumbers] as a range. If only one episode is
|
/// Render this [`EpisodeNumbers`] as a range. If only one episode is
|
||||||
/// is present it renders as `01`, if multiple it renders as `01-02`
|
/// is present it renders as `01`, if multiple it renders as `01-02`.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn range_string(&self) -> String {
|
pub fn range_string(&self) -> String {
|
||||||
let start = self.0.start();
|
let start = self.0.start();
|
||||||
let end = self.0.end();
|
let end = self.0.end();
|
||||||
|
|
||||||
if start == end {
|
if start == end {
|
||||||
format!("{:02}", start)
|
format!("{start:02}")
|
||||||
} else {
|
} else {
|
||||||
format!("{:02}-{:02}", start, end)
|
format!("{start:02}-{end:02}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{EpisodeNumber, EpisodeNumbers, SeasonNumber};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_fn() {
|
||||||
|
_ = SeasonNumber::new(0);
|
||||||
|
let e = EpisodeNumber::new(0);
|
||||||
|
_ = EpisodeNumbers::new(e, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+48
-13
@@ -1,22 +1,26 @@
|
|||||||
//! This module contains helper functions for normalizing media titles
|
//! This module contains helper functions for normalizing media titles.
|
||||||
|
|
||||||
use core::iter::Peekable;
|
use core::iter::Peekable;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
/// Return an iterator over the normalized words of a string.
|
||||||
|
/// - Alphanumeric
|
||||||
|
/// - Lowercase
|
||||||
|
/// - Replace `&` with `and`
|
||||||
|
/// - Collapse acronyms
|
||||||
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `input` is not ASCII.
|
/// Panics if `input` is not ASCII.
|
||||||
fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
|
fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
|
||||||
if !input.is_ascii() {
|
assert!(input.is_ascii(), "input is not ASCII: {input}");
|
||||||
panic!("Input is not ASCII: {input}");
|
|
||||||
}
|
|
||||||
|
|
||||||
input
|
input
|
||||||
.split_ascii_whitespace()
|
.split_ascii_whitespace()
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
if s == "&" {
|
if s == "&" {
|
||||||
return "and".to_string();
|
return "and".to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
let chars = s
|
let chars = s
|
||||||
@@ -37,6 +41,7 @@ fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
|
|||||||
.filter(|part: &String| !part.is_empty() && part != "-")
|
.filter(|part: &String| !part.is_empty() && part != "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split out `a`, `an`, and `the` from the start of the string if it exists.
|
||||||
fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>, Peekable<I>) {
|
fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>, Peekable<I>) {
|
||||||
let mut iter = iter.peekable();
|
let mut iter = iter.peekable();
|
||||||
match iter.peek().map(String::as_str) {
|
match iter.peek().map(String::as_str) {
|
||||||
@@ -57,13 +62,15 @@ fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `input` is not ASCII.
|
/// Panics if `input` is not ASCII.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn make_sortable_title(title: &str) -> String {
|
pub fn make_sortable_title(title: &str) -> String {
|
||||||
let words = split_normalized_words(title);
|
let words = split_normalized_words(title);
|
||||||
let (article, words) = split_leading_article(words);
|
let (article, words) = split_leading_article(words);
|
||||||
|
|
||||||
let output = Itertools::intersperse(words, " ".to_string());
|
let output = Itertools::intersperse(words, " ".to_owned());
|
||||||
if let Some(article) = article {
|
if let Some(article) = article {
|
||||||
output.chain([", ".to_string(), article]).collect()
|
output.chain([", ".to_owned(), article]).collect()
|
||||||
} else {
|
} else {
|
||||||
output.collect()
|
output.collect()
|
||||||
}
|
}
|
||||||
@@ -81,11 +88,13 @@ pub fn make_sortable_title(title: &str) -> String {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `input` is not ASCII.
|
/// Panics if `input` is not ASCII.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn make_fs_slug(title: &str) -> String {
|
pub fn make_fs_slug(title: &str) -> String {
|
||||||
let words = split_normalized_words(title);
|
let words = split_normalized_words(title);
|
||||||
let (_, words) = split_leading_article(words);
|
let (_, words) = split_leading_article(words);
|
||||||
|
|
||||||
Itertools::intersperse(words, " ".to_string()).collect()
|
Itertools::intersperse(words, " ".to_owned()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a media title and year to a folder name representable on filesystems
|
/// Convert a media title and year to a folder name representable on filesystems
|
||||||
@@ -100,11 +109,13 @@ pub fn make_fs_slug(title: &str) -> String {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `input` is not ASCII.
|
/// Panics if `input` is not ASCII.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn make_fs_slug_year(title: &str, year: i32) -> String {
|
pub fn make_fs_slug_year(title: &str, year: i32) -> String {
|
||||||
let words = split_normalized_words(title);
|
let words = split_normalized_words(title);
|
||||||
let (_, words) = split_leading_article(words);
|
let (_, words) = split_leading_article(words);
|
||||||
|
|
||||||
Itertools::intersperse(words, " ".to_string())
|
Itertools::intersperse(words, " ".to_owned())
|
||||||
.chain([format!(" ({year})")])
|
.chain([format!(" ({year})")])
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -117,10 +128,12 @@ pub fn make_fs_slug_year(title: &str, year: i32) -> String {
|
|||||||
/// assert_eq!(normalize_fs_name("Marvel's Agents of SHIELD (2013)"), "marvels agents of shield (2013)");
|
/// assert_eq!(normalize_fs_name("Marvel's Agents of SHIELD (2013)"), "marvels agents of shield (2013)");
|
||||||
/// assert_eq!(normalize_fs_name("Avatar The Last Airbender (2005)"), "avatar the last airbender (2005)");
|
/// assert_eq!(normalize_fs_name("Avatar The Last Airbender (2005)"), "avatar the last airbender (2005)");
|
||||||
/// assert_eq!(normalize_fs_name("Cloak & Dagger (2018)"), "cloak and dagger (2018)");
|
/// assert_eq!(normalize_fs_name("Cloak & Dagger (2018)"), "cloak and dagger (2018)");
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn normalize_fs_name(input: &str) -> String {
|
pub fn normalize_fs_name(input: &str) -> String {
|
||||||
let chars = input.split_ascii_whitespace().map(|s| {
|
let chars = input.split_ascii_whitespace().map(|s| {
|
||||||
if s == "&" {
|
if s == "&" {
|
||||||
return "and".to_string();
|
return "and".to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
let chars = s
|
let chars = s
|
||||||
@@ -138,7 +151,7 @@ pub fn normalize_fs_name(input: &str) -> String {
|
|||||||
chars.collect()
|
chars.collect()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Itertools::intersperse(chars, " ".to_string()).collect()
|
Itertools::intersperse(chars, " ".to_owned()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a media title to a url compatible string
|
/// Convert a media title to a url compatible string
|
||||||
@@ -153,11 +166,13 @@ pub fn normalize_fs_name(input: &str) -> String {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `input` is not ASCII.
|
/// Panics if `input` is not ASCII.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn make_web_slug(title: &str) -> String {
|
pub fn make_web_slug(title: &str) -> String {
|
||||||
let words = split_normalized_words(title);
|
let words = split_normalized_words(title);
|
||||||
let (_, words) = split_leading_article(words);
|
let (_, words) = split_leading_article(words);
|
||||||
|
|
||||||
Itertools::intersperse(words, "-".to_string()).collect()
|
Itertools::intersperse(words, "-".to_owned()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a media title and year to a url compatible string
|
/// Convert a media title and year to a url compatible string
|
||||||
@@ -172,11 +187,31 @@ pub fn make_web_slug(title: &str) -> String {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `input` is not ASCII.
|
/// Panics if `input` is not ASCII.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn make_web_slug_year(title: &str, year: i32) -> String {
|
pub fn make_web_slug_year(title: &str, year: i32) -> String {
|
||||||
let words = split_normalized_words(title);
|
let words = split_normalized_words(title);
|
||||||
let (_, words) = split_leading_article(words);
|
let (_, words) = split_leading_article(words);
|
||||||
|
|
||||||
Itertools::intersperse(words, "-".to_string())
|
Itertools::intersperse(words, "-".to_owned())
|
||||||
.chain([format!("-{year}")])
|
.chain([format!("-{year}")])
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
make_fs_slug, make_fs_slug_year, make_sortable_title, make_web_slug, make_web_slug_year,
|
||||||
|
normalize_fs_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_fn() {
|
||||||
|
drop(make_sortable_title(""));
|
||||||
|
drop(make_fs_slug(""));
|
||||||
|
drop(make_fs_slug_year("", 0));
|
||||||
|
drop(normalize_fs_name(""));
|
||||||
|
drop(make_web_slug(""));
|
||||||
|
drop(make_web_slug_year("", 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flix-tmdb"
|
name = "flix-tmdb"
|
||||||
version = "0.0.20"
|
version = "0.1.0"
|
||||||
license-file.workspace = true
|
|
||||||
|
|
||||||
description = "Clients and models for fetching data from TMDB"
|
description = "Clients and models for fetching data from TMDB"
|
||||||
repository = "https://github.com/QuantumShade/flix"
|
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||||
categories = []
|
keywords = ["flix"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
include = ["/src"]
|
||||||
|
publish = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -32,6 +34,7 @@ sea-orm = { workspace = true, optional = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_test = { workspace = true }
|
serde_test = { workspace = true }
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Collections API
|
//! Collections API.
|
||||||
|
|
||||||
use std::rc::Rc;
|
use alloc::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::api::exec_request;
|
use crate::api::exec_request;
|
||||||
@@ -10,26 +10,38 @@ use crate::{Cache, CachePolicy, Config};
|
|||||||
|
|
||||||
use super::{Error, make_request};
|
use super::{Error, make_request};
|
||||||
|
|
||||||
/// TMDB Collections API client
|
/// TMDB Collections API client.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
config: Rc<Config>,
|
/// The client configuration.
|
||||||
cache: Rc<dyn Cache>,
|
config: Arc<Config>,
|
||||||
policy: Rc<RwLock<CachePolicy>>,
|
/// The request cache.
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
/// The cache policy to use for requests.
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration.
|
||||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
config: Arc<Config>,
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
cache,
|
cache,
|
||||||
policy,
|
policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
/// Fetch the details of the collection refered to by ID.
|
||||||
/// Fetch the details of the collection refered to by ID
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||||
pub async fn get_details(
|
pub async fn get_details(
|
||||||
&self,
|
&self,
|
||||||
id: impl Into<CollectionId>,
|
id: impl Into<CollectionId>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Episodes API
|
//! Episodes API.
|
||||||
|
|
||||||
use std::rc::Rc;
|
use alloc::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
||||||
@@ -12,26 +12,38 @@ use crate::{Cache, CachePolicy, Config};
|
|||||||
|
|
||||||
use super::{Error, make_request};
|
use super::{Error, make_request};
|
||||||
|
|
||||||
/// TMDB Episodes API client
|
/// TMDB Episodes API client.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
config: Rc<Config>,
|
/// The client configuration.
|
||||||
cache: Rc<dyn Cache>,
|
config: Arc<Config>,
|
||||||
policy: Rc<RwLock<CachePolicy>>,
|
/// The request cache.
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
/// The cache policy to use for requests.
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration.
|
||||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
config: Arc<Config>,
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
cache,
|
cache,
|
||||||
policy,
|
policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
/// Fetch the details of the episode refered to by ID, season number and episode number.
|
||||||
/// Fetch the details of the episode refered to by ID, season number and episode number
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||||
pub async fn get_details(
|
pub async fn get_details(
|
||||||
&self,
|
&self,
|
||||||
id: impl Into<ShowId>,
|
id: impl Into<ShowId>,
|
||||||
|
|||||||
+31
-17
@@ -1,6 +1,5 @@
|
|||||||
//! TMDB API clients
|
//! TMDB API clients.
|
||||||
|
|
||||||
use core::ops::Deref;
|
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
@@ -17,20 +16,26 @@ pub mod movies;
|
|||||||
pub mod seasons;
|
pub mod seasons;
|
||||||
pub mod shows;
|
pub mod shows;
|
||||||
|
|
||||||
/// A generic error wrapping Url and Reqwest errors
|
/// A generic error wrapping Url and Reqwest errors.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[expect(clippy::error_impl_error, reason = "Error is a good name here")]
|
||||||
|
#[expect(clippy::exhaustive_enums, reason = "unlikely to add new variants")]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Url error wrapper
|
/// Url error wrapper.
|
||||||
#[error("url parse error: {0}")]
|
#[error("url parse error: {0}")]
|
||||||
Url(#[from] url::ParseError),
|
Url(#[from] url::ParseError),
|
||||||
/// Reqwest error wrapper
|
/// Reqwest error wrapper.
|
||||||
#[error("reqwest error: {0}")]
|
#[error("reqwest error: {0}")]
|
||||||
Reqwest(#[from] reqwest::Error),
|
Reqwest(#[from] reqwest::Error),
|
||||||
/// Json error wrapper
|
/// Json error wrapper.
|
||||||
#[error("json error: {0}")]
|
#[error("json error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn a [Config] and `path` into a [Request].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<Request, Error> {
|
fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<Request, Error> {
|
||||||
let url = config.base_url.join(path)?;
|
let url = config.base_url.join(path)?;
|
||||||
|
|
||||||
@@ -38,7 +43,7 @@ fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<R
|
|||||||
header::AUTHORIZATION,
|
header::AUTHORIZATION,
|
||||||
format!("Bearer {}", config.bearer_token),
|
format!("Bearer {}", config.bearer_token),
|
||||||
);
|
);
|
||||||
if let Some(ref user_agent) = config.user_agent {
|
if let Some(user_agent) = &config.user_agent {
|
||||||
builder = builder.header(header::USER_AGENT, user_agent);
|
builder = builder.header(header::USER_AGENT, user_agent);
|
||||||
}
|
}
|
||||||
if let Some(language) = language {
|
if let Some(language) = language {
|
||||||
@@ -48,22 +53,22 @@ fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<R
|
|||||||
Ok(builder.build()?)
|
Ok(builder.build()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a [Request] and deserialize the response.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
async fn exec_request<T: DeserializeOwned>(
|
async fn exec_request<T: DeserializeOwned>(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
cache: &dyn Cache,
|
cache: &dyn Cache,
|
||||||
policy: &RwLock<CachePolicy>,
|
policy: &RwLock<CachePolicy>,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> Result<T, Error> {
|
) -> Result<T, Error> {
|
||||||
let (read_cache, write_cache) = if let Ok(guard) = policy.read() {
|
let (read_cache, write_cache) = policy.read().map_or((None, None), |guard| match *guard {
|
||||||
match guard.deref() {
|
|
||||||
CachePolicy::None => (None, None),
|
CachePolicy::None => (None, None),
|
||||||
CachePolicy::Full => (Some(cache), Some(cache)),
|
CachePolicy::Full => (Some(cache), Some(cache)),
|
||||||
CachePolicy::Read => (Some(cache), None),
|
CachePolicy::Read => (Some(cache), None),
|
||||||
CachePolicy::Update => (None, Some(cache)),
|
CachePolicy::Update => (None, Some(cache)),
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = request.url().path().to_owned();
|
let path = request.url().path().to_owned();
|
||||||
|
|
||||||
@@ -73,9 +78,9 @@ async fn exec_request<T: DeserializeOwned>(
|
|||||||
response = cache.get(&path);
|
response = cache.get(&path);
|
||||||
}
|
}
|
||||||
let needs_cache_write = response.is_none();
|
let needs_cache_write = response.is_none();
|
||||||
let response = match response {
|
let response = if let Some(response) = response {
|
||||||
Some(response) => response,
|
response
|
||||||
None => {
|
} else {
|
||||||
config
|
config
|
||||||
.limiter
|
.limiter
|
||||||
.until_ready_with_jitter(Jitter::new(
|
.until_ready_with_jitter(Jitter::new(
|
||||||
@@ -90,7 +95,6 @@ async fn exec_request<T: DeserializeOwned>(
|
|||||||
.error_for_status()?
|
.error_for_status()?
|
||||||
.bytes()
|
.bytes()
|
||||||
.await?
|
.await?
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// write to the cache if needed
|
// write to the cache if needed
|
||||||
@@ -102,3 +106,13 @@ async fn exec_request<T: DeserializeOwned>(
|
|||||||
|
|
||||||
Ok(serde_json::from_slice(&response)?)
|
Ok(serde_json::from_slice(&response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Error;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
drop(Error::Url(url::ParseError::Overflow));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Movies API
|
//! Movies API.
|
||||||
|
|
||||||
use std::rc::Rc;
|
use alloc::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::api::exec_request;
|
use crate::api::exec_request;
|
||||||
@@ -10,26 +10,38 @@ use crate::{Cache, CachePolicy, Config};
|
|||||||
|
|
||||||
use super::{Error, make_request};
|
use super::{Error, make_request};
|
||||||
|
|
||||||
/// TMDB Movies API client
|
/// TMDB Movies API client.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
config: Rc<Config>,
|
/// The client configuration.
|
||||||
cache: Rc<dyn Cache>,
|
config: Arc<Config>,
|
||||||
policy: Rc<RwLock<CachePolicy>>,
|
/// The request cache.
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
/// The cache policy to use for requests.
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration.
|
||||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
config: Arc<Config>,
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
cache,
|
cache,
|
||||||
policy,
|
policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
/// Fetch the details of the movie refered to by ID.
|
||||||
/// Fetch the details of the movie refered to by ID
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||||
pub async fn get_details(
|
pub async fn get_details(
|
||||||
&self,
|
&self,
|
||||||
id: impl Into<MovieId>,
|
id: impl Into<MovieId>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Seasons API
|
//! Seasons API.
|
||||||
|
|
||||||
use std::rc::Rc;
|
use alloc::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use flix_model::numbers::SeasonNumber;
|
use flix_model::numbers::SeasonNumber;
|
||||||
@@ -12,26 +12,38 @@ use crate::{Cache, CachePolicy, Config};
|
|||||||
|
|
||||||
use super::{Error, make_request};
|
use super::{Error, make_request};
|
||||||
|
|
||||||
/// TMDB Seasons API client
|
/// TMDB Seasons API client.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
config: Rc<Config>,
|
/// The client configuration.
|
||||||
cache: Rc<dyn Cache>,
|
config: Arc<Config>,
|
||||||
policy: Rc<RwLock<CachePolicy>>,
|
/// The request cache.
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
/// The cache policy to use for requests.
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration.
|
||||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
config: Arc<Config>,
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
cache,
|
cache,
|
||||||
policy,
|
policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
/// Fetch the details of the season refered to by ID and season number.
|
||||||
/// Fetch the details of the season refered to by ID and season number
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||||
pub async fn get_details(
|
pub async fn get_details(
|
||||||
&self,
|
&self,
|
||||||
id: impl Into<ShowId>,
|
id: impl Into<ShowId>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Shows API
|
//! Shows API.
|
||||||
|
|
||||||
use std::rc::Rc;
|
use alloc::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::api::exec_request;
|
use crate::api::exec_request;
|
||||||
@@ -10,26 +10,38 @@ use crate::{Cache, CachePolicy, Config};
|
|||||||
|
|
||||||
use super::{Error, make_request};
|
use super::{Error, make_request};
|
||||||
|
|
||||||
/// TMDB Shows API client
|
/// TMDB Shows API client.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
config: Rc<Config>,
|
/// The client configuration.
|
||||||
cache: Rc<dyn Cache>,
|
config: Arc<Config>,
|
||||||
policy: Rc<RwLock<CachePolicy>>,
|
/// The request cache.
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
/// The cache policy to use for requests.
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration.
|
||||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
config: Arc<Config>,
|
||||||
|
cache: Arc<dyn Cache>,
|
||||||
|
policy: Arc<RwLock<CachePolicy>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
cache,
|
cache,
|
||||||
policy,
|
policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
/// Fetch the details of the show refered to by ID.
|
||||||
/// Fetch the details of the show refered to by ID
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// See [Error] for failure modes.
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||||
pub async fn get_details(
|
pub async fn get_details(
|
||||||
&self,
|
&self,
|
||||||
id: impl Into<ShowId>,
|
id: impl Into<ShowId>,
|
||||||
|
|||||||
+33
-20
@@ -1,58 +1,72 @@
|
|||||||
|
//! Caching related types.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use redb::{Database, DatabaseError, ReadableDatabase, TableDefinition};
|
use redb::{Database, DatabaseError, ReadableDatabase as _, TableDefinition};
|
||||||
|
|
||||||
/// The client cache policy
|
/// The client cache policy.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum CachePolicy {
|
pub enum CachePolicy {
|
||||||
/// Do not use a cache
|
/// Do not use a cache.
|
||||||
None,
|
None,
|
||||||
/// Use and update the cache
|
/// Use and update the cache.
|
||||||
Full,
|
Full,
|
||||||
/// Use the cache but don't update it
|
/// Use the cache but don't update it.
|
||||||
Read,
|
Read,
|
||||||
/// Ignore the cache but update it
|
/// Ignore the cache but update it.
|
||||||
Update,
|
Update,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The trait representing a caching backend
|
/// The trait representing a caching backend.
|
||||||
pub trait Cache {
|
pub trait Cache: core::fmt::Debug + Send + Sync {
|
||||||
/// Get a cached value, or None
|
/// Get a cached value, or None.
|
||||||
fn get(&self, query: &str) -> Option<Bytes>;
|
fn get(&self, query: &str) -> Option<Bytes>;
|
||||||
/// Set a value in the cache
|
/// Set a value in the cache.
|
||||||
fn set(&self, query: &str, response: &Bytes);
|
fn set(&self, query: &str, response: &Bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABLE: TableDefinition<&str, (u64, &[u8])> = TableDefinition::new("tmdb_responses");
|
/// The [`TableDefinition`] for TMDB response data.
|
||||||
|
const TABLE: TableDefinition<'_, &str, (u64, &[u8])> = TableDefinition::new("tmdb_responses");
|
||||||
|
|
||||||
/// A [Cache] implementation using [redb] as the backend
|
/// A [Cache] implementation using [redb] as the backend.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct RedbCache {
|
pub struct RedbCache {
|
||||||
|
/// The underlying database.
|
||||||
db: Database,
|
db: Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedbCache {
|
impl RedbCache {
|
||||||
/// Create/open a [redb] database at the path
|
/// Create/open a [redb] database at the path.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Fails if creating/opening the database returns an error.
|
||||||
|
#[inline]
|
||||||
pub fn new(path: &Path) -> Result<Self, DatabaseError> {
|
pub fn new(path: &Path) -> Result<Self, DatabaseError> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: Database::create(path)?,
|
db: Database::create(path)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function allowing for `.ok()?`
|
/// Helper function allowing for `.ok()?`.
|
||||||
fn write(&self, timestamp: u64, query: &str, response: &Bytes) -> Option<()> {
|
fn write(&self, timestamp: u64, query: &str, response: &Bytes) -> Option<()> {
|
||||||
let write_txn = self.db.begin_write().ok()?;
|
let write_txn = self.db.begin_write().ok()?;
|
||||||
{
|
{
|
||||||
let mut table = write_txn.open_table(TABLE).ok()?;
|
let mut table = write_txn.open_table(TABLE).ok()?;
|
||||||
|
drop(
|
||||||
table
|
table
|
||||||
.insert(query, (timestamp, response.iter().as_slice()))
|
.insert(query, (timestamp, response.iter().as_slice()))
|
||||||
.ok()?;
|
.ok()?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
write_txn.commit().ok()
|
write_txn.commit().ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache for RedbCache {
|
impl Cache for RedbCache {
|
||||||
|
#[inline]
|
||||||
fn get(&self, query: &str) -> Option<Bytes> {
|
fn get(&self, query: &str) -> Option<Bytes> {
|
||||||
let read_txn = self.db.begin_read().ok()?;
|
let read_txn = self.db.begin_read().ok()?;
|
||||||
let table = read_txn.open_table(TABLE).ok()?;
|
let table = read_txn.open_table(TABLE).ok()?;
|
||||||
@@ -62,8 +76,7 @@ impl Cache for RedbCache {
|
|||||||
|
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map_or(0, |d| d.as_secs());
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
if now.saturating_sub(timestamp) >= 60 * 60 * 24 * 30 * 6 {
|
if now.saturating_sub(timestamp) >= 60 * 60 * 24 * 30 * 6 {
|
||||||
None
|
None
|
||||||
@@ -72,12 +85,12 @@ impl Cache for RedbCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn set(&self, query: &str, response: &Bytes) {
|
fn set(&self, query: &str, response: &Bytes) {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map_or(0, |d| d.as_secs());
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
self.write(now, query, response);
|
_ = self.write(now, query, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-30
@@ -1,44 +1,63 @@
|
|||||||
use std::rc::Rc;
|
//! TMDB client.
|
||||||
|
|
||||||
|
use alloc::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::{Cache, CachePolicy, Config, api};
|
use crate::{Cache, CachePolicy, Config, api};
|
||||||
|
|
||||||
/// The primary client that references all other clients
|
/// The primary client that references all other clients.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
|
/// The collection API client.
|
||||||
collections: api::collections::Client,
|
collections: api::collections::Client,
|
||||||
|
/// The movie API client.
|
||||||
movies: api::movies::Client,
|
movies: api::movies::Client,
|
||||||
|
/// The show API client.
|
||||||
shows: api::shows::Client,
|
shows: api::shows::Client,
|
||||||
|
/// The season API client.
|
||||||
seasons: api::seasons::Client,
|
seasons: api::seasons::Client,
|
||||||
|
/// The episode API client.
|
||||||
episodes: api::episodes::Client,
|
episodes: api::episodes::Client,
|
||||||
|
|
||||||
cache_policy: Rc<RwLock<CachePolicy>>,
|
/// The cache policy to use for all clients.
|
||||||
|
cache_policy: Arc<RwLock<CachePolicy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration.
|
||||||
pub fn new(config: Config, cache: Rc<dyn Cache>, cache_policy: CachePolicy) -> Self {
|
#[inline]
|
||||||
let config = Rc::new(config);
|
pub fn new(config: Config, cache: Arc<dyn Cache>, cache_policy: CachePolicy) -> Self {
|
||||||
let cache_policy = Rc::new(RwLock::new(cache_policy));
|
let config = Arc::new(config);
|
||||||
|
let cache_policy = Arc::new(RwLock::new(cache_policy));
|
||||||
Self {
|
Self {
|
||||||
collections: api::collections::Client::new(
|
collections: api::collections::Client::new(
|
||||||
config.clone(),
|
Arc::clone(&config),
|
||||||
cache.clone(),
|
Arc::clone(&cache),
|
||||||
cache_policy.clone(),
|
Arc::clone(&cache_policy),
|
||||||
),
|
),
|
||||||
movies: api::movies::Client::new(config.clone(), cache.clone(), cache_policy.clone()),
|
movies: api::movies::Client::new(
|
||||||
shows: api::shows::Client::new(config.clone(), cache.clone(), cache_policy.clone()),
|
Arc::clone(&config),
|
||||||
seasons: api::seasons::Client::new(config.clone(), cache.clone(), cache_policy.clone()),
|
Arc::clone(&cache),
|
||||||
episodes: api::episodes::Client::new(
|
Arc::clone(&cache_policy),
|
||||||
config.clone(),
|
|
||||||
cache.clone(),
|
|
||||||
cache_policy.clone(),
|
|
||||||
),
|
),
|
||||||
|
shows: api::shows::Client::new(
|
||||||
|
Arc::clone(&config),
|
||||||
|
Arc::clone(&cache),
|
||||||
|
Arc::clone(&cache_policy),
|
||||||
|
),
|
||||||
|
seasons: api::seasons::Client::new(
|
||||||
|
Arc::clone(&config),
|
||||||
|
Arc::clone(&cache),
|
||||||
|
Arc::clone(&cache_policy),
|
||||||
|
),
|
||||||
|
episodes: api::episodes::Client::new(config, cache, Arc::clone(&cache_policy)),
|
||||||
|
|
||||||
cache_policy,
|
cache_policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modify the [CachePolicy]
|
/// Modify the [`CachePolicy`].
|
||||||
|
#[inline]
|
||||||
pub fn set_cache_policy(&self, new_policy: CachePolicy) {
|
pub fn set_cache_policy(&self, new_policy: CachePolicy) {
|
||||||
match self.cache_policy.write() {
|
match self.cache_policy.write() {
|
||||||
Ok(mut policy) => *policy = new_policy,
|
Ok(mut policy) => *policy = new_policy,
|
||||||
@@ -48,31 +67,39 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
/// Access the Collections API.
|
||||||
/// Access the Collections API
|
#[inline]
|
||||||
pub fn collections(&self) -> &api::collections::Client {
|
#[must_use]
|
||||||
|
pub const fn collections(&self) -> &api::collections::Client {
|
||||||
&self.collections
|
&self.collections
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the Movies API
|
/// Access the Movies API.
|
||||||
pub fn movies(&self) -> &api::movies::Client {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn movies(&self) -> &api::movies::Client {
|
||||||
&self.movies
|
&self.movies
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the Shows API
|
/// Access the Shows API.
|
||||||
pub fn shows(&self) -> &api::shows::Client {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn shows(&self) -> &api::shows::Client {
|
||||||
&self.shows
|
&self.shows
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the Seasons API
|
/// Access the Seasons API.
|
||||||
pub fn seasons(&self) -> &api::seasons::Client {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn seasons(&self) -> &api::seasons::Client {
|
||||||
&self.seasons
|
&self.seasons
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the Episodes API
|
/// Access the Episodes API.
|
||||||
pub fn episodes(&self) -> &api::episodes::Client {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn episodes(&self) -> &api::episodes::Client {
|
||||||
&self.episodes
|
&self.episodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Client configuration.
|
||||||
|
|
||||||
use governor::clock::MonotonicClock;
|
use governor::clock::MonotonicClock;
|
||||||
use governor::state::{InMemoryState, NotKeyed};
|
use governor::state::{InMemoryState, NotKeyed};
|
||||||
use governor::{Quota, RateLimiter};
|
use governor::{Quota, RateLimiter};
|
||||||
@@ -5,27 +7,31 @@ use nonzero_ext::nonzero;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use url_macro::url;
|
use url_macro::url;
|
||||||
|
|
||||||
/// The client configuration
|
/// The client configuration.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[expect(clippy::exhaustive_structs, reason = "must be constructed in full")]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// The base URL of the API
|
/// The base URL of the API.
|
||||||
pub base_url: Url,
|
pub base_url: Url,
|
||||||
/// The reqwest client that is used for every request
|
/// The reqwest client that is used for every request.
|
||||||
pub client: reqwest::Client,
|
pub client: reqwest::Client,
|
||||||
/// The rate limiter to use for the client
|
/// The rate limiter to use for the client.
|
||||||
pub limiter: RateLimiter<NotKeyed, InMemoryState, MonotonicClock>,
|
pub limiter: RateLimiter<NotKeyed, InMemoryState, MonotonicClock>,
|
||||||
/// The bearer token for readonly access to the API
|
/// The bearer token for readonly access to the API.
|
||||||
pub bearer_token: String,
|
pub bearer_token: String,
|
||||||
/// An optional user agent string to provide to the API
|
/// An optional user agent string to provide to the API.
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Create a new configuration using the provided bearer token
|
/// Create a new configuration using the provided bearer token.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn new(bearer_token: String) -> Self {
|
pub fn new(bearer_token: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base_url: url!("https://api.themoviedb.org"),
|
base_url: url!("https://api.themoviedb.org"),
|
||||||
client: reqwest::Client::new(),
|
client: reqwest::Client::new(),
|
||||||
limiter: RateLimiter::direct(Quota::per_second(nonzero!(30u32))),
|
limiter: RateLimiter::direct(Quota::per_second(nonzero!(30_u32))),
|
||||||
bearer_token,
|
bearer_token,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -1,7 +1,9 @@
|
|||||||
//! flix-tmdb provides clients and models for fetching data from TMDB
|
//! flix-tmdb provides clients and models for fetching data from TMDB.
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
|
||||||
@@ -13,3 +15,20 @@ pub use client::Client;
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
|
||||||
|
use super::{CachePolicy, Client, Config, RedbCache};
|
||||||
|
|
||||||
|
#[cfg(not(miri))]
|
||||||
|
#[test]
|
||||||
|
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||||
|
fn use_types() {
|
||||||
|
let tmp = tempfile::tempdir().expect("temporary directory exists");
|
||||||
|
let config = Config::new(String::new());
|
||||||
|
let cache = Arc::new(RedbCache::new(&tmp.path().join("cache.redb")).expect("redb"));
|
||||||
|
drop(Client::new(config, cache, CachePolicy::Full));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
|
//! TMDB collection model.
|
||||||
|
|
||||||
use super::id::{CollectionId, MovieId};
|
use super::id::{CollectionId, MovieId};
|
||||||
|
|
||||||
/// A deserialized Collection from the TMDB API
|
/// A deserialized Collection from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Collection {
|
pub struct Collection {
|
||||||
/// The collection's TMDB ID
|
/// The collection's TMDB ID.
|
||||||
pub id: CollectionId,
|
pub id: CollectionId,
|
||||||
/// The collection's title
|
/// The collection's title.
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The collection's overview
|
/// The collection's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The list of movies that are part of this collection
|
/// The list of movies that are part of this collection.
|
||||||
#[serde(rename = "parts")]
|
#[serde(rename = "parts")]
|
||||||
pub movies: Vec<Item>,
|
pub movies: Vec<Item>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A deserialized collection item from the TMDB API
|
/// A deserialized collection item from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
/// The movie's TMDB ID
|
/// The movie's TMDB ID.
|
||||||
pub id: MovieId,
|
pub id: MovieId,
|
||||||
/// The movie's title
|
/// The movie's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Collection, CollectionId, Item, MovieId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
drop(Collection {
|
||||||
|
id: CollectionId::from_raw(0),
|
||||||
|
title: String::new(),
|
||||||
|
overview: String::new(),
|
||||||
|
movies: Vec::new(),
|
||||||
|
});
|
||||||
|
drop(Item {
|
||||||
|
id: MovieId::from_raw(0),
|
||||||
|
title: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! TMDB episode model.
|
||||||
|
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
use flix_model::numbers::EpisodeNumber;
|
use flix_model::numbers::EpisodeNumber;
|
||||||
@@ -7,22 +9,44 @@ use url::Url;
|
|||||||
|
|
||||||
use super::{duration_from_minutes, still_url_from_path};
|
use super::{duration_from_minutes, still_url_from_path};
|
||||||
|
|
||||||
/// A deserialized Episode from the TMDB API
|
/// A deserialized Episode from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Episode {
|
pub struct Episode {
|
||||||
/// The episode's number
|
/// The episode's number.
|
||||||
pub episode_number: EpisodeNumber,
|
pub episode_number: EpisodeNumber,
|
||||||
/// The episode's title
|
/// The episode's title.
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The episode's overview
|
/// The episode's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The episode's air date
|
/// The episode's air date.
|
||||||
pub air_date: NaiveDate,
|
pub air_date: NaiveDate,
|
||||||
/// The episode's runtime
|
/// The episode's runtime.
|
||||||
#[serde(deserialize_with = "duration_from_minutes")]
|
#[serde(deserialize_with = "duration_from_minutes")]
|
||||||
pub runtime: Duration,
|
pub runtime: Duration,
|
||||||
/// The episode's still path
|
/// The episode's still path.
|
||||||
#[serde(deserialize_with = "still_url_from_path")]
|
#[serde(deserialize_with = "still_url_from_path")]
|
||||||
pub still_path: Option<Url>,
|
pub still_path: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use super::{Episode, EpisodeNumber};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
drop(Episode {
|
||||||
|
episode_number: EpisodeNumber::new(0),
|
||||||
|
title: String::new(),
|
||||||
|
overview: String::new(),
|
||||||
|
air_date: NaiveDate::default(),
|
||||||
|
runtime: Duration::default(),
|
||||||
|
still_path: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+63
-24
@@ -1,4 +1,6 @@
|
|||||||
//! Typed TMDB IDs
|
//! Typed TMDB IDs.
|
||||||
|
|
||||||
|
#![expect(clippy::module_name_repetitions, reason = "ID types end with Id")]
|
||||||
|
|
||||||
use core::cmp::Ordering;
|
use core::cmp::Ordering;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
@@ -10,21 +12,25 @@ use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
|
|||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
|
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
|
||||||
|
|
||||||
/// The internal representation used by TMDB
|
/// The internal representation used by TMDB.
|
||||||
pub type TmdbRepr = u32;
|
pub type TmdbRepr = u32;
|
||||||
|
|
||||||
/// An opaque type representing a TMDB ID
|
/// An opaque type representing a TMDB ID.
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct Id<T> {
|
pub struct Id<T> {
|
||||||
|
/// Internal ID representation.
|
||||||
id: TmdbRepr,
|
id: TmdbRepr,
|
||||||
|
/// Marker to indicate an invariant T.
|
||||||
#[serde(skip_serializing, default)]
|
#[serde(skip_serializing, default)]
|
||||||
_phantom: PhantomData<T>,
|
_phantom: PhantomData<fn(T) -> T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual implementation since `T: Clone` is not required
|
// Manual implementation since `T: Clone` is not required
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> Clone for Id<T> {
|
impl<T> Clone for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
@@ -34,31 +40,40 @@ impl<T> Clone for Id<T> {
|
|||||||
impl<T> Copy for Id<T> {}
|
impl<T> Copy for Id<T> {}
|
||||||
|
|
||||||
// Manual implementation since `T: PartialEq` is not required
|
// Manual implementation since `T: PartialEq` is not required
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> PartialEq for Id<T> {
|
impl<T> PartialEq for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.id == other.id
|
self.id == other.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual implementation since `T: Eq` is not required
|
// Manual implementation since `T: Eq` is not required
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> Eq for Id<T> {}
|
impl<T> Eq for Id<T> {}
|
||||||
|
|
||||||
// Manual implementation since `T: PartialOrd` is not required
|
// Manual implementation since `T: PartialOrd` is not required
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> PartialOrd for Id<T> {
|
impl<T> PartialOrd for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual implementation since `T: Ord` is not required
|
// Manual implementation since `T: Ord` is not required
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> Ord for Id<T> {
|
impl<T> Ord for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
self.id.cmp(&other.id)
|
self.id.cmp(&other.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual implementation since `T: Hash` is not required
|
// Manual implementation since `T: Hash` is not required
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> Hash for Id<T> {
|
impl<T> Hash for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.id.hash(state);
|
self.id.hash(state);
|
||||||
}
|
}
|
||||||
@@ -66,6 +81,8 @@ impl<T> Hash for Id<T> {
|
|||||||
|
|
||||||
impl<T> Id<T> {
|
impl<T> Id<T> {
|
||||||
/// Allows the conversion from a raw value to [Id], though the use is discouraged.
|
/// Allows the conversion from a raw value to [Id], though the use is discouraged.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn from_raw(raw: TmdbRepr) -> Self {
|
pub fn from_raw(raw: TmdbRepr) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: raw,
|
id: raw,
|
||||||
@@ -74,13 +91,16 @@ impl<T> Id<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Allows extracting the raw value, though the use is discouraged.
|
/// Allows extracting the raw value, though the use is discouraged.
|
||||||
pub fn into_raw(self) -> TmdbRepr {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn into_raw(self) -> TmdbRepr {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> fmt::Debug for Id<T> {
|
impl<T> fmt::Debug for Id<T> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("Id")
|
f.debug_struct("Id")
|
||||||
.field("T", &core::any::type_name::<T>())
|
.field("T", &core::any::type_name::<T>())
|
||||||
.field("id", &self.id)
|
.field("id", &self.id)
|
||||||
@@ -89,7 +109,9 @@ impl<T> fmt::Debug for Id<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> ValueType for Id<T> {
|
impl<T> ValueType for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
|
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
|
||||||
<TmdbRepr as ValueType>::try_from(v).map(|id| Self {
|
<TmdbRepr as ValueType>::try_from(v).map(|id| Self {
|
||||||
id,
|
id,
|
||||||
@@ -97,14 +119,17 @@ impl<T> ValueType for Id<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn type_name() -> String {
|
fn type_name() -> String {
|
||||||
format!("Id<{}>", &core::any::type_name::<T>())
|
format!("Id<{}>", core::any::type_name::<T>())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn array_type() -> ArrayType {
|
fn array_type() -> ArrayType {
|
||||||
TmdbRepr::array_type()
|
TmdbRepr::array_type()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn column_type() -> ColumnType {
|
fn column_type() -> ColumnType {
|
||||||
TmdbRepr::column_type()
|
TmdbRepr::column_type()
|
||||||
}
|
}
|
||||||
@@ -112,13 +137,16 @@ impl<T> ValueType for Id<T> {
|
|||||||
|
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
impl<T> From<Id<T>> for Value {
|
impl<T> From<Id<T>> for Value {
|
||||||
|
#[inline]
|
||||||
fn from(value: Id<T>) -> Self {
|
fn from(value: Id<T>) -> Self {
|
||||||
value.id.into()
|
value.id.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl<T> TryGetable for Id<T> {
|
impl<T> TryGetable for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
|
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
|
||||||
TmdbRepr::try_get_by(res, index).map(|id| Self {
|
TmdbRepr::try_get_by(res, index).map(|id| Self {
|
||||||
id,
|
id,
|
||||||
@@ -129,6 +157,7 @@ impl<T> TryGetable for Id<T> {
|
|||||||
|
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
impl<T> TryFromU64 for Id<T> {
|
impl<T> TryFromU64 for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||||
TmdbRepr::try_from_u64(n).map(|id| Self {
|
TmdbRepr::try_from_u64(n).map(|id| Self {
|
||||||
id,
|
id,
|
||||||
@@ -139,6 +168,7 @@ impl<T> TryFromU64 for Id<T> {
|
|||||||
|
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
impl<T> Nullable for Id<T> {
|
impl<T> Nullable for Id<T> {
|
||||||
|
#[inline]
|
||||||
fn null() -> Value {
|
fn null() -> Value {
|
||||||
TmdbRepr::null()
|
TmdbRepr::null()
|
||||||
}
|
}
|
||||||
@@ -146,34 +176,34 @@ impl<T> Nullable for Id<T> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
fn test_sea_orm() {
|
#[expect(dead_code, reason = "structs test derive macros")]
|
||||||
|
mod test_seaorm {
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
|
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
|
||||||
EnumIter, PrimaryKeyTrait,
|
EnumIter, PrimaryKeyTrait,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Id;
|
use super::super::Id;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "ids")]
|
#[sea_orm(table_name = "ids")]
|
||||||
pub struct Model {
|
#[expect(clippy::use_self, reason = "derive macros cause Self to be invalid")]
|
||||||
|
struct Model {
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
id: Id<Model>,
|
id: Id<Model>,
|
||||||
nullable: Option<Id<Model>>,
|
nullable: Option<Id<Model>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, EnumIter, DeriveRelation)]
|
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
enum Relation {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serde() {
|
fn serde() {
|
||||||
use super::Id;
|
use super::Id;
|
||||||
|
|
||||||
let id: Id<()> = Id::from_raw(1234);
|
let id: Id<()> = Id::from_raw(1234);
|
||||||
@@ -181,61 +211,70 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type alias for the raw ID representation
|
|
||||||
pub use self::TmdbRepr as RawId;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum Collection {}
|
pub enum Collection {}
|
||||||
/// Type alias for a collection ID
|
/// Type alias for a collection ID.
|
||||||
pub type CollectionId = Id<Collection>;
|
pub type CollectionId = Id<Collection>;
|
||||||
|
|
||||||
impl From<CollectionId> for flix_model::id::CollectionId {
|
impl From<CollectionId> for flix_model::id::CollectionId {
|
||||||
|
#[inline]
|
||||||
fn from(value: CollectionId) -> Self {
|
fn from(value: CollectionId) -> Self {
|
||||||
Self::from_raw(value.into_raw().into())
|
Self::from_raw(value.into_raw().into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<flix_model::id::CollectionId> for CollectionId {
|
impl TryFrom<flix_model::id::CollectionId> for CollectionId {
|
||||||
type Error = <RawId as TryFrom<flix_model::id::RawId>>::Error;
|
type Error = <TmdbRepr as TryFrom<flix_model::id::RawId>>::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn try_from(value: flix_model::id::CollectionId) -> Result<Self, Self::Error> {
|
fn try_from(value: flix_model::id::CollectionId) -> Result<Self, Self::Error> {
|
||||||
value.into_raw().try_into().map(Self::from_raw)
|
value.into_raw().try_into().map(Self::from_raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum Movie {}
|
pub enum Movie {}
|
||||||
/// Type alias for a movie ID
|
/// Type alias for a movie ID.
|
||||||
pub type MovieId = Id<Movie>;
|
pub type MovieId = Id<Movie>;
|
||||||
|
|
||||||
impl From<MovieId> for flix_model::id::MovieId {
|
impl From<MovieId> for flix_model::id::MovieId {
|
||||||
|
#[inline]
|
||||||
fn from(value: MovieId) -> Self {
|
fn from(value: MovieId) -> Self {
|
||||||
Self::from_raw(value.into_raw().into())
|
Self::from_raw(value.into_raw().into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<flix_model::id::MovieId> for MovieId {
|
impl TryFrom<flix_model::id::MovieId> for MovieId {
|
||||||
type Error = <RawId as TryFrom<flix_model::id::RawId>>::Error;
|
type Error = <TmdbRepr as TryFrom<flix_model::id::RawId>>::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn try_from(value: flix_model::id::MovieId) -> Result<Self, Self::Error> {
|
fn try_from(value: flix_model::id::MovieId) -> Result<Self, Self::Error> {
|
||||||
value.into_raw().try_into().map(Self::from_raw)
|
value.into_raw().try_into().map(Self::from_raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum Show {}
|
pub enum Show {}
|
||||||
/// Type alias for a show ID
|
/// Type alias for a show ID.
|
||||||
pub type ShowId = Id<Show>;
|
pub type ShowId = Id<Show>;
|
||||||
|
|
||||||
impl From<ShowId> for flix_model::id::ShowId {
|
impl From<ShowId> for flix_model::id::ShowId {
|
||||||
|
#[inline]
|
||||||
fn from(value: ShowId) -> Self {
|
fn from(value: ShowId) -> Self {
|
||||||
Self::from_raw(value.into_raw().into())
|
Self::from_raw(value.into_raw().into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<flix_model::id::ShowId> for ShowId {
|
impl TryFrom<flix_model::id::ShowId> for ShowId {
|
||||||
type Error = <RawId as TryFrom<flix_model::id::RawId>>::Error;
|
type Error = <TmdbRepr as TryFrom<flix_model::id::RawId>>::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn try_from(value: flix_model::id::ShowId) -> Result<Self, Self::Error> {
|
fn try_from(value: flix_model::id::ShowId) -> Result<Self, Self::Error> {
|
||||||
value.into_raw().try_into().map(Self::from_raw)
|
value.into_raw().try_into().map(Self::from_raw)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! Deserializable types from the TMDB API
|
//! Deserializable types from the TMDB API.
|
||||||
|
|
||||||
use core::str::FromStr;
|
use core::str::FromStr as _;
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize as _, Deserializer};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub mod id;
|
pub mod id;
|
||||||
@@ -20,14 +20,22 @@ pub use movie::*;
|
|||||||
pub use season::*;
|
pub use season::*;
|
||||||
pub use show::*;
|
pub use show::*;
|
||||||
|
|
||||||
|
/// Deserializer for converting integer minutes to a [Duration].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Fails if the value being deserialized isn't a [u64].
|
||||||
fn duration_from_minutes<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
fn duration_from_minutes<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let minutes = u64::deserialize(deserializer).unwrap_or(0);
|
let minutes = u64::deserialize(deserializer)?;
|
||||||
Ok(Duration::from_secs(minutes.saturating_mul(60)))
|
Ok(Duration::from_secs(minutes.saturating_mul(60)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserializer for converting a string to a [Url].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Fails if the value being deserialized isn't a [`&str`].
|
||||||
fn still_url_from_path<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
|
fn still_url_from_path<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
@@ -38,10 +46,6 @@ where
|
|||||||
let path = Option::<&str>::deserialize(deserializer)?;
|
let path = Option::<&str>::deserialize(deserializer)?;
|
||||||
|
|
||||||
Ok(path.and_then(|path| {
|
Ok(path.and_then(|path| {
|
||||||
Url::from_str(&format!(
|
Url::from_str(&format!("{TMDB_IMAGE_BASE}{TMDB_IMAGE_QUALITY}{path}")).ok()
|
||||||
"{}{}{}",
|
|
||||||
TMDB_IMAGE_BASE, TMDB_IMAGE_QUALITY, path
|
|
||||||
))
|
|
||||||
.ok()
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! TMDB movie model.
|
||||||
|
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
@@ -5,33 +7,35 @@ use chrono::NaiveDate;
|
|||||||
use super::duration_from_minutes;
|
use super::duration_from_minutes;
|
||||||
use super::id::{CollectionId, MovieId};
|
use super::id::{CollectionId, MovieId};
|
||||||
|
|
||||||
/// A deserialized Movie from the TMDB API
|
/// A deserialized Movie from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Movie {
|
pub struct Movie {
|
||||||
/// The movie's TMDB ID
|
/// The movie's TMDB ID.
|
||||||
pub id: MovieId,
|
pub id: MovieId,
|
||||||
/// The movie's collection, if it exists
|
/// The movie's collection, if it exists.
|
||||||
#[serde(rename = "belongs_to_collection")]
|
#[serde(rename = "belongs_to_collection")]
|
||||||
pub collection: Option<InCollection>,
|
pub collection: Option<InCollection>,
|
||||||
/// The movie's title
|
/// The movie's title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The movie's tagline
|
/// The movie's tagline.
|
||||||
pub tagline: String,
|
pub tagline: String,
|
||||||
/// The movie's overview
|
/// The movie's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The movie's release date
|
/// The movie's release date.
|
||||||
pub release_date: NaiveDate,
|
pub release_date: NaiveDate,
|
||||||
/// The movie's runtime
|
/// The movie's runtime.
|
||||||
#[serde(deserialize_with = "duration_from_minutes")]
|
#[serde(deserialize_with = "duration_from_minutes")]
|
||||||
pub runtime: Duration,
|
pub runtime: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A deserialized movie's collection from the TMDB API
|
/// A deserialized movie's collection from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct InCollection {
|
pub struct InCollection {
|
||||||
/// The collection's TMDB ID
|
/// The collection's TMDB ID.
|
||||||
pub id: CollectionId,
|
pub id: CollectionId,
|
||||||
/// The collection's title
|
/// The collection's title.
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
@@ -43,3 +47,28 @@ pub struct InCollection {
|
|||||||
// TODO: Company
|
// TODO: Company
|
||||||
// pub companies: Vec<Company>
|
// pub companies: Vec<Company>
|
||||||
// where: struct Company { id, name }
|
// where: struct Company { id, name }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use super::{CollectionId, InCollection, Movie, MovieId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
drop(Movie {
|
||||||
|
id: MovieId::from_raw(0),
|
||||||
|
collection: Some(InCollection {
|
||||||
|
id: CollectionId::from_raw(0),
|
||||||
|
title: String::new(),
|
||||||
|
}),
|
||||||
|
title: String::new(),
|
||||||
|
tagline: String::new(),
|
||||||
|
overview: String::new(),
|
||||||
|
release_date: NaiveDate::default(),
|
||||||
|
runtime: Duration::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,46 @@
|
|||||||
|
//! TMDB season model.
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use flix_model::numbers::SeasonNumber;
|
use flix_model::numbers::SeasonNumber;
|
||||||
|
|
||||||
/// A deserialized Season from the TMDB API
|
/// A deserialized Season from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Season {
|
pub struct Season {
|
||||||
/// The season's number
|
/// The season's number.
|
||||||
pub season_number: SeasonNumber,
|
pub season_number: SeasonNumber,
|
||||||
/// The season's title
|
/// The season's title.
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The season's overview
|
/// The season's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The season's air date
|
/// The season's air date.
|
||||||
pub air_date: NaiveDate,
|
pub air_date: NaiveDate,
|
||||||
/// The number of episodes in this season
|
/// The number of episodes in this season.
|
||||||
pub episodes: Vec<FakeEpisode>,
|
pub episodes: Vec<FakeEpisode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A placeholder struct for parsing the episodes list for a season
|
/// A placeholder struct for parsing the episodes list for a season.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[expect(clippy::empty_structs_with_brackets, reason = "might add fields later")]
|
||||||
pub struct FakeEpisode {}
|
pub struct FakeEpisode {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use super::{Season, SeasonNumber};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
drop(Season {
|
||||||
|
season_number: SeasonNumber::new(0),
|
||||||
|
title: String::new(),
|
||||||
|
overview: String::new(),
|
||||||
|
air_date: NaiveDate::default(),
|
||||||
|
episodes: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
|
//! TMDB show model.
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use super::id::ShowId;
|
use super::id::ShowId;
|
||||||
|
|
||||||
/// A deserialized Show from the TMDB API
|
/// A deserialized Show from the TMDB API.
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Show {
|
pub struct Show {
|
||||||
/// The show's TMDB ID
|
/// The show's TMDB ID.
|
||||||
pub id: ShowId,
|
pub id: ShowId,
|
||||||
/// The show's title
|
/// The show's title.
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// The show's tagline
|
/// The show's tagline.
|
||||||
pub tagline: String,
|
pub tagline: String,
|
||||||
/// The show's overview
|
/// The show's overview.
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
/// The show's first air date
|
/// The show's first air date.
|
||||||
pub first_air_date: NaiveDate,
|
pub first_air_date: NaiveDate,
|
||||||
/// The show's last air date
|
/// The show's last air date.
|
||||||
pub last_air_date: NaiveDate,
|
pub last_air_date: NaiveDate,
|
||||||
/// The total number of episodes in this show
|
/// The total number of episodes in this show.
|
||||||
pub number_of_episodes: u32,
|
pub number_of_episodes: u32,
|
||||||
/// The number of seasons in this show
|
/// The number of seasons in this show.
|
||||||
pub number_of_seasons: u32,
|
pub number_of_seasons: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,3 +38,24 @@ pub struct Show {
|
|||||||
// TODO: Company
|
// TODO: Company
|
||||||
// pub companies: Vec<Company>
|
// pub companies: Vec<Company>
|
||||||
// where: struct Company { id, name }
|
// where: struct Company { id, name }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use super::{Show, ShowId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_types() {
|
||||||
|
drop(Show {
|
||||||
|
id: ShowId::from_raw(0),
|
||||||
|
title: String::new(),
|
||||||
|
tagline: String::new(),
|
||||||
|
overview: String::new(),
|
||||||
|
first_air_date: NaiveDate::default(),
|
||||||
|
last_air_date: NaiveDate::default(),
|
||||||
|
number_of_episodes: 0,
|
||||||
|
number_of_seasons: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
set minimum-version := '1.55.0'
|
||||||
|
set default-list
|
||||||
|
|
||||||
|
alias update := update-min
|
||||||
|
|
||||||
|
all:
|
||||||
|
just all-min
|
||||||
|
just all-max
|
||||||
|
all-min: update-min build lint test example doc format
|
||||||
|
all-max: update-max build lint test example doc format
|
||||||
|
|
||||||
|
update-min:
|
||||||
|
cargo +nightly update -Z direct-minimal-versions
|
||||||
|
|
||||||
|
update-max:
|
||||||
|
cargo update
|
||||||
|
|
||||||
|
build:
|
||||||
|
cargo hack --feature-powerset build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
cargo hack --feature-powerset clippy -- -D warnings
|
||||||
|
cargo hack --feature-powerset clippy --tests -- -D warnings
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo hack --feature-powerset test
|
||||||
|
cargo +1.94 hack --feature-powerset test
|
||||||
|
cargo +nightly hack --feature-powerset miri test
|
||||||
|
|
||||||
|
example:
|
||||||
|
# cargo run --example=<example> --features=<req_features>
|
||||||
|
|
||||||
|
doc *args:
|
||||||
|
RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc \
|
||||||
|
--workspace --all-features --document-private-items \
|
||||||
|
--lib --bins --no-deps --locked \
|
||||||
|
{{ args }}
|
||||||
|
|
||||||
|
format:
|
||||||
|
cargo fmt --check
|
||||||
|
|
||||||
|
install:
|
||||||
|
cargo install --path crates/cli
|
||||||
|
cargo install --path crates/cli-mux
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "stable"
|
channel = "1.98"
|
||||||
|
|||||||
Reference in New Issue
Block a user