domain: introduce provider-neutral media model (phase 1)
Establish src-tauri/src/domain/ as the single source of truth for the media model, with all Jellyfin translation isolated in from_jellyfin.rs. Adds MediaKind enum and neutral duration_ms/image_id fields to MediaItem as additive, defaulted dual-carry alongside the legacy Jellyfin-named fields, so nothing breaks while the frontend migrates off them. - domain/media.rs: canonical MediaKind (closed enum, replaces stringly item_type), Default = Other so unknown/defaulted items are inert. - domain/from_jellyfin.rs: total, panic-free item_type -> MediaKind classification (all audited types + person subroles) and ticks->ms. - MediaItem gains kind/duration_ms/image_id, populated at both mapping seams (online to_media_item, offline cached_item_to_media_item) and the synthesized-album/person sites. - Regenerated bindings.ts: frontend now HAS the neutral model available. Phase 1 of docs/specs/frontend-domain-model.md. No frontend behaviour change yet; wire shape is a superset of before. Rust 456 tests, frontend 644 tests, check + check:boundary all green.
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"@vitest/ui": "^4.0.16",
|
"@vitest/ui": "^4.0.16",
|
||||||
"@wdio/cli": "^9.5.0",
|
"@wdio/cli": "^9.5.0",
|
||||||
"@wdio/local-runner": "^9.5.0",
|
"@wdio/local-runner": "^9.5.0",
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.3",
|
||||||
"vitest": "^4.0.16",
|
"vitest": ">=1.0.0 <5.0.0",
|
||||||
"webdriverio": "^9.5.0",
|
"webdriverio": "^9.5.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -46,10 +47,18 @@
|
|||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||||
|
|
||||||
|
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
||||||
|
|
||||||
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
|
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
|
||||||
|
|
||||||
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
|
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
|
||||||
@@ -348,6 +357,8 @@
|
|||||||
|
|
||||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.10", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.10", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.10", "vitest": "4.1.10" }, "optionalPeers": ["@vitest/browser"] }, "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="],
|
"@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="],
|
||||||
|
|
||||||
"@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="],
|
"@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="],
|
||||||
@@ -362,7 +373,7 @@
|
|||||||
|
|
||||||
"@vitest/ui": ["@vitest/ui@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "vitest": "4.0.16" } }, "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg=="],
|
"@vitest/ui": ["@vitest/ui@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "vitest": "4.0.16" } }, "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg=="],
|
||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
"@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
|
||||||
|
|
||||||
"@wdio/cli": ["@wdio/cli@9.23.2", "", { "dependencies": { "@vitest/snapshot": "^2.1.1", "@wdio/config": "9.23.2", "@wdio/globals": "9.23.0", "@wdio/logger": "9.18.0", "@wdio/protocols": "9.23.2", "@wdio/types": "9.23.2", "@wdio/utils": "9.23.2", "async-exit-hook": "^2.0.1", "chalk": "^5.4.1", "chokidar": "^4.0.0", "create-wdio": "9.21.0", "dotenv": "^17.2.0", "import-meta-resolve": "^4.0.0", "lodash.flattendeep": "^4.4.0", "lodash.pickby": "^4.6.0", "lodash.union": "^4.6.0", "read-pkg-up": "^10.0.0", "tsx": "^4.7.2", "webdriverio": "9.23.2", "yargs": "^17.7.2" }, "bin": { "wdio": "bin/wdio.js" } }, "sha512-D6KZGomfNmjFhSWYdfR7Ojik5qWEpPoR4g5LQPzbFwiii/RkTudLcMFcCO6s7HTMLDQDWryOStV2KK6KqrIF8A=="],
|
"@wdio/cli": ["@wdio/cli@9.23.2", "", { "dependencies": { "@vitest/snapshot": "^2.1.1", "@wdio/config": "9.23.2", "@wdio/globals": "9.23.0", "@wdio/logger": "9.18.0", "@wdio/protocols": "9.23.2", "@wdio/types": "9.23.2", "@wdio/utils": "9.23.2", "async-exit-hook": "^2.0.1", "chalk": "^5.4.1", "chokidar": "^4.0.0", "create-wdio": "9.21.0", "dotenv": "^17.2.0", "import-meta-resolve": "^4.0.0", "lodash.flattendeep": "^4.4.0", "lodash.pickby": "^4.6.0", "lodash.union": "^4.6.0", "read-pkg-up": "^10.0.0", "tsx": "^4.7.2", "webdriverio": "9.23.2", "yargs": "^17.7.2" }, "bin": { "wdio": "bin/wdio.js" } }, "sha512-D6KZGomfNmjFhSWYdfR7Ojik5qWEpPoR4g5LQPzbFwiii/RkTudLcMFcCO6s7HTMLDQDWryOStV2KK6KqrIF8A=="],
|
||||||
|
|
||||||
@@ -422,6 +433,8 @@
|
|||||||
|
|
||||||
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
|
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
|
||||||
|
|
||||||
|
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA=="],
|
||||||
|
|
||||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
"async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="],
|
"async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="],
|
||||||
@@ -498,6 +511,8 @@
|
|||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||||
|
|
||||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
@@ -686,6 +701,8 @@
|
|||||||
|
|
||||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||||
|
|
||||||
|
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
|
||||||
|
|
||||||
"htmlfy": ["htmlfy@0.8.1", "", {}, "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ=="],
|
"htmlfy": ["htmlfy@0.8.1", "", {}, "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ=="],
|
||||||
|
|
||||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||||
@@ -738,6 +755,12 @@
|
|||||||
|
|
||||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||||
|
|
||||||
|
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
|
||||||
|
|
||||||
|
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
|
||||||
|
|
||||||
|
"istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
|
||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
||||||
@@ -756,7 +779,7 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
@@ -828,6 +851,10 @@
|
|||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
|
||||||
|
|
||||||
|
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
|
||||||
|
|
||||||
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
@@ -1020,7 +1047,7 @@
|
|||||||
|
|
||||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||||
|
|
||||||
"stream-buffers": ["stream-buffers@3.0.3", "", {}, "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw=="],
|
"stream-buffers": ["stream-buffers@3.0.3", "", {}, "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw=="],
|
||||||
|
|
||||||
@@ -1042,7 +1069,7 @@
|
|||||||
|
|
||||||
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"svelte": ["svelte@5.48.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ=="],
|
"svelte": ["svelte@5.48.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ=="],
|
||||||
|
|
||||||
@@ -1068,7 +1095,7 @@
|
|||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||||
|
|
||||||
"tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="],
|
"tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="],
|
||||||
|
|
||||||
@@ -1166,6 +1193,10 @@
|
|||||||
|
|
||||||
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
|
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
|
||||||
|
|
||||||
|
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||||
|
|
||||||
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||||
|
|
||||||
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
@@ -1190,10 +1221,24 @@
|
|||||||
|
|
||||||
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||||
|
|
||||||
|
"@vitest/expect/@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||||
|
|
||||||
|
"@vitest/expect/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
|
"@vitest/pretty-format/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
|
"@vitest/runner/@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||||
|
|
||||||
"@vitest/snapshot/@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="],
|
"@vitest/snapshot/@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="],
|
||||||
|
|
||||||
"@vitest/snapshot/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
"@vitest/snapshot/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||||
|
|
||||||
|
"@vitest/ui/@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||||
|
|
||||||
|
"@vitest/ui/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
|
"@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
|
||||||
|
|
||||||
"@wdio/reporter/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
"@wdio/reporter/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
@@ -1260,6 +1305,8 @@
|
|||||||
|
|
||||||
"mocha/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
|
"mocha/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
|
||||||
|
|
||||||
|
"mocha/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||||
|
|
||||||
"mocha/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
"mocha/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||||
|
|
||||||
"mocha/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
"mocha/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||||
@@ -1298,6 +1345,12 @@
|
|||||||
|
|
||||||
"vitest/@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="],
|
"vitest/@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="],
|
||||||
|
|
||||||
|
"vitest/@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||||
|
|
||||||
|
"vitest/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||||
|
|
||||||
|
"vitest/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
"wait-port/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"wait-port/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"wait-port/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
"wait-port/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
||||||
@@ -1328,7 +1381,7 @@
|
|||||||
|
|
||||||
"@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"@vitest/runner/@vitest/utils/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
"@vitest/snapshot/@vitest/pretty-format/tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="],
|
"@vitest/snapshot/@vitest/pretty-format/tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="],
|
||||||
|
|
||||||
@@ -1338,34 +1391,24 @@
|
|||||||
|
|
||||||
"jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"jest-diff/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"jest-diff/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
"jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
"jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
"jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"mocha/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"mocha/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
"mocha/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"mocha/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
@@ -1432,8 +1475,6 @@
|
|||||||
|
|
||||||
"wait-port/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"wait-port/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"wait-port/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"mocha/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"mocha/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"mocha/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
"mocha/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
//! Jellyfin → domain translation.
|
||||||
|
//!
|
||||||
|
//! The ONLY place Jellyfin's vocabulary touches the domain model. Adding a
|
||||||
|
//! second provider later means a sibling `from_<provider>.rs`; the domain types
|
||||||
|
//! and every consumer stay untouched.
|
||||||
|
//!
|
||||||
|
//! Spec: docs/specs/frontend-domain-model.md
|
||||||
|
|
||||||
|
use super::media::MediaKind;
|
||||||
|
|
||||||
|
/// Jellyfin ticks per second (10 million). A tick is 100 ns.
|
||||||
|
/// The frontend must never see ticks — this is where they die.
|
||||||
|
const TICKS_PER_MILLISECOND: i64 = 10_000;
|
||||||
|
|
||||||
|
/// Convert a Jellyfin `RunTimeTicks` value to milliseconds.
|
||||||
|
///
|
||||||
|
/// Domain durations are milliseconds; ticks are a Jellyfin unit and stop here.
|
||||||
|
pub fn ticks_to_ms(ticks: i64) -> i64 {
|
||||||
|
ticks / TICKS_PER_MILLISECOND
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify a Jellyfin `Type` string into a neutral [`MediaKind`].
|
||||||
|
///
|
||||||
|
/// **Total and panic-free**: any unrecognised string maps to [`MediaKind::Other`]
|
||||||
|
/// rather than failing. `is_folder` disambiguates the one Jellyfin type
|
||||||
|
/// (`ChannelFolderItem`) whose kind depends on whether it is a container.
|
||||||
|
///
|
||||||
|
/// The recognised set is every `item_type` the frontend audit found in use
|
||||||
|
/// (docs/specs/frontend-domain-model.md), plus the common cast/crew person
|
||||||
|
/// subtypes Jellyfin returns in `People[].Type`.
|
||||||
|
pub fn kind_from_jellyfin(item_type: &str, is_folder: bool) -> MediaKind {
|
||||||
|
match item_type {
|
||||||
|
// Music
|
||||||
|
"Audio" | "MusicVideo" => MediaKind::Track,
|
||||||
|
"MusicAlbum" => MediaKind::Album,
|
||||||
|
"MusicArtist" | "AlbumArtist" => MediaKind::Artist,
|
||||||
|
"Playlist" => MediaKind::Playlist,
|
||||||
|
|
||||||
|
// Video
|
||||||
|
"Movie" => MediaKind::Movie,
|
||||||
|
"Series" => MediaKind::Series,
|
||||||
|
"Season" => MediaKind::Season,
|
||||||
|
"Episode" => MediaKind::Episode,
|
||||||
|
// A bare video leaf with no richer classification.
|
||||||
|
"Video" => MediaKind::Movie,
|
||||||
|
|
||||||
|
// Cast / crew — Jellyfin uses both a "Person" item type and role-typed
|
||||||
|
// people (Actor/Director/Writer/Composer/…) in People[].Type.
|
||||||
|
"Person" | "Actor" | "Director" | "Writer" | "Composer" | "GuestStar" | "Producer" => {
|
||||||
|
MediaKind::Person
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live TV / channels
|
||||||
|
"TvChannel" | "LiveTvChannel" | "Channel" => MediaKind::Channel,
|
||||||
|
|
||||||
|
// Containers
|
||||||
|
"Folder" | "CollectionFolder" | "UserView" | "BoxSet" => MediaKind::Folder,
|
||||||
|
// ChannelFolderItem is a container when it is a folder, else a leaf we
|
||||||
|
// do not model further.
|
||||||
|
"ChannelFolderItem" => {
|
||||||
|
if is_folder {
|
||||||
|
MediaKind::Folder
|
||||||
|
} else {
|
||||||
|
MediaKind::Other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown → safe sink. Never panics.
|
||||||
|
_ => {
|
||||||
|
if is_folder {
|
||||||
|
MediaKind::Folder
|
||||||
|
} else {
|
||||||
|
MediaKind::Other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ticks_convert_to_milliseconds() {
|
||||||
|
// 1 second = 10,000,000 ticks = 1000 ms
|
||||||
|
assert_eq!(ticks_to_ms(10_000_000), 1000);
|
||||||
|
// 90.5 s
|
||||||
|
assert_eq!(ticks_to_ms(905_000_000), 90_500);
|
||||||
|
assert_eq!(ticks_to_ms(0), 0);
|
||||||
|
// Sub-millisecond truncates toward zero, not panics.
|
||||||
|
assert_eq!(ticks_to_ms(9_999), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn music_types_map() {
|
||||||
|
assert_eq!(kind_from_jellyfin("Audio", false), MediaKind::Track);
|
||||||
|
assert_eq!(kind_from_jellyfin("MusicAlbum", true), MediaKind::Album);
|
||||||
|
assert_eq!(kind_from_jellyfin("MusicArtist", true), MediaKind::Artist);
|
||||||
|
assert_eq!(kind_from_jellyfin("Playlist", true), MediaKind::Playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn video_types_map() {
|
||||||
|
assert_eq!(kind_from_jellyfin("Movie", false), MediaKind::Movie);
|
||||||
|
assert_eq!(kind_from_jellyfin("Series", true), MediaKind::Series);
|
||||||
|
assert_eq!(kind_from_jellyfin("Season", true), MediaKind::Season);
|
||||||
|
assert_eq!(kind_from_jellyfin("Episode", false), MediaKind::Episode);
|
||||||
|
assert_eq!(kind_from_jellyfin("Video", false), MediaKind::Movie);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn person_and_role_types_map_to_person() {
|
||||||
|
for t in ["Person", "Actor", "Director", "Writer", "Composer"] {
|
||||||
|
assert_eq!(kind_from_jellyfin(t, false), MediaKind::Person, "{t}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn channel_and_container_types_map() {
|
||||||
|
assert_eq!(kind_from_jellyfin("TvChannel", false), MediaKind::Channel);
|
||||||
|
assert_eq!(
|
||||||
|
kind_from_jellyfin("CollectionFolder", true),
|
||||||
|
MediaKind::Folder
|
||||||
|
);
|
||||||
|
assert_eq!(kind_from_jellyfin("BoxSet", true), MediaKind::Folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn channel_folder_item_disambiguates_on_is_folder() {
|
||||||
|
assert_eq!(
|
||||||
|
kind_from_jellyfin("ChannelFolderItem", true),
|
||||||
|
MediaKind::Folder
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
kind_from_jellyfin("ChannelFolderItem", false),
|
||||||
|
MediaKind::Other
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_type_never_panics_and_falls_back() {
|
||||||
|
// The whole point: garbage in, safe kind out, no panic.
|
||||||
|
assert_eq!(kind_from_jellyfin("Epis0de", false), MediaKind::Other);
|
||||||
|
assert_eq!(kind_from_jellyfin("", false), MediaKind::Other);
|
||||||
|
assert_eq!(
|
||||||
|
kind_from_jellyfin("SomeFutureType", true),
|
||||||
|
MediaKind::Folder
|
||||||
|
);
|
||||||
|
assert_eq!(kind_from_jellyfin("🎵unicode", false), MediaKind::Other);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//! Canonical, provider-neutral media domain model.
|
||||||
|
//!
|
||||||
|
//! This is the *single source of truth* for what a media item is across the
|
||||||
|
//! whole app. Rust (repositories, player, downloads) uses these types directly;
|
||||||
|
//! the frontend consumes the tauri-specta-generated projection in
|
||||||
|
//! `src/lib/api/bindings.ts`. There is no second hand-written copy in either
|
||||||
|
//! language, so the model cannot drift.
|
||||||
|
//!
|
||||||
|
//! No provider (Jellyfin) vocabulary belongs in this file. Translation from a
|
||||||
|
//! provider's wire shape lives beside it in `from_jellyfin.rs` and is the only
|
||||||
|
//! place provider terms touch the domain type.
|
||||||
|
//!
|
||||||
|
//! Spec: docs/specs/frontend-domain-model.md
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The kind of a media item — provider-neutral classification.
|
||||||
|
///
|
||||||
|
/// Replaces the stringly-typed `item_type` that carried Jellyfin's vocabulary
|
||||||
|
/// (`"Audio"`, `"MusicAlbum"`, …) across the boundary. A closed enum means a
|
||||||
|
/// typo or an unhandled kind is a compile error on the frontend, not a silent
|
||||||
|
/// runtime miss across ~127 comparison sites.
|
||||||
|
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum MediaKind {
|
||||||
|
// Music
|
||||||
|
Track,
|
||||||
|
Album,
|
||||||
|
Artist,
|
||||||
|
Playlist,
|
||||||
|
// Video
|
||||||
|
Movie,
|
||||||
|
Series,
|
||||||
|
Season,
|
||||||
|
Episode,
|
||||||
|
// Cast/crew
|
||||||
|
Person,
|
||||||
|
// Containers / live TV
|
||||||
|
Channel,
|
||||||
|
Folder,
|
||||||
|
/// A kind we do not model explicitly. Reached only for provider item types
|
||||||
|
/// that map to nothing meaningful; consumers treat it like an opaque
|
||||||
|
/// container. The mapping must be *total* — it never panics — so this is the
|
||||||
|
/// safe sink for unknown strings. Also the `Default`, so a defaulted
|
||||||
|
/// `MediaItem` (see the dual-carry migration) is inert rather than a lie.
|
||||||
|
#[default]
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaKind {
|
||||||
|
/// True for kinds that are containers/collections rather than playable leaves.
|
||||||
|
/// Presentation-neutral helper the backend can use for e.g. drill-vs-play.
|
||||||
|
// Consumed by later migration phases (drill-vs-play routing); kept now so the
|
||||||
|
// domain surface is complete alongside the type it describes.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn is_container(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
MediaKind::Album
|
||||||
|
| MediaKind::Artist
|
||||||
|
| MediaKind::Series
|
||||||
|
| MediaKind::Season
|
||||||
|
| MediaKind::Playlist
|
||||||
|
| MediaKind::Channel
|
||||||
|
| MediaKind::Folder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Canonical, provider-neutral domain model — the single source of truth for
|
||||||
|
//! the app's core data shapes, shared with the frontend via generated bindings.
|
||||||
|
//!
|
||||||
|
//! Spec: docs/specs/frontend-domain-model.md
|
||||||
|
|
||||||
|
pub mod from_jellyfin;
|
||||||
|
pub mod media;
|
||||||
|
|
||||||
|
pub use from_jellyfin::{kind_from_jellyfin, ticks_to_ms};
|
||||||
|
pub use media::MediaKind;
|
||||||
@@ -2,6 +2,7 @@ mod auth;
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod connectivity;
|
mod connectivity;
|
||||||
mod credentials;
|
mod credentials;
|
||||||
|
mod domain;
|
||||||
mod download;
|
mod download;
|
||||||
mod jellyfin;
|
mod jellyfin;
|
||||||
mod playback_mode;
|
mod playback_mode;
|
||||||
|
|||||||
@@ -2177,6 +2177,7 @@ mod tests {
|
|||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
name: format!("Episode {}", index),
|
name: format!("Episode {}", index),
|
||||||
item_type: "Episode".to_string(),
|
item_type: "Episode".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Episode,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: "server".to_string(),
|
server_id: "server".to_string(),
|
||||||
parent_id: Some("season1".to_string()),
|
parent_id: Some("season1".to_string()),
|
||||||
@@ -2184,11 +2185,13 @@ mod tests {
|
|||||||
overview: None,
|
overview: None,
|
||||||
genres: None,
|
genres: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
production_year: None,
|
production_year: None,
|
||||||
premiere_date: None,
|
premiere_date: None,
|
||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
|
|||||||
@@ -1422,6 +1422,7 @@ mod tests {
|
|||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
item_type: "Movie".to_string(),
|
item_type: "Movie".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Movie,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: "test-server".to_string(),
|
server_id: "test-server".to_string(),
|
||||||
parent_id: Some("parent-123".to_string()),
|
parent_id: Some("parent-123".to_string()),
|
||||||
@@ -1429,11 +1430,13 @@ mod tests {
|
|||||||
overview: Some("Test overview".to_string()),
|
overview: Some("Test overview".to_string()),
|
||||||
genres: Some(vec!["Action".to_string(), "Adventure".to_string()]),
|
genres: Some(vec!["Action".to_string(), "Adventure".to_string()]),
|
||||||
runtime_ticks: Some(7200000000),
|
runtime_ticks: Some(7200000000),
|
||||||
|
duration_ms: Some(720000),
|
||||||
production_year: Some(2024),
|
production_year: Some(2024),
|
||||||
premiere_date: None,
|
premiere_date: None,
|
||||||
community_rating: Some(8.5),
|
community_rating: Some(8.5),
|
||||||
official_rating: Some("PG-13".to_string()),
|
official_rating: Some("PG-13".to_string()),
|
||||||
primary_image_tag: Some("image-tag-123".to_string()),
|
primary_image_tag: Some("image-tag-123".to_string()),
|
||||||
|
image_id: Some("image-tag-123".to_string()),
|
||||||
backdrop_image_tags: Some(vec!["backdrop-1".to_string()]),
|
backdrop_image_tags: Some(vec!["backdrop-1".to_string()]),
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
|
|||||||
@@ -68,10 +68,13 @@ impl OfflineRepository {
|
|||||||
.and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
|
.and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let kind = crate::domain::kind_from_jellyfin(&item.item_type, item.is_folder);
|
||||||
|
|
||||||
MediaItem {
|
MediaItem {
|
||||||
id: item.id.clone(),
|
id: item.id.clone(),
|
||||||
name: item.name,
|
name: item.name,
|
||||||
item_type: item.item_type,
|
item_type: item.item_type,
|
||||||
|
kind,
|
||||||
is_folder: item.is_folder,
|
is_folder: item.is_folder,
|
||||||
server_id: item.server_id,
|
server_id: item.server_id,
|
||||||
parent_id: item.parent_id,
|
parent_id: item.parent_id,
|
||||||
@@ -82,11 +85,13 @@ impl OfflineRepository {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|s| serde_json::from_str::<Vec<String>>(s).ok()),
|
.and_then(|s| serde_json::from_str::<Vec<String>>(s).ok()),
|
||||||
runtime_ticks: item.runtime_ticks,
|
runtime_ticks: item.runtime_ticks,
|
||||||
|
duration_ms: item.runtime_ticks.map(crate::domain::ticks_to_ms),
|
||||||
production_year: item.production_year,
|
production_year: item.production_year,
|
||||||
premiere_date: item.premiere_date,
|
premiere_date: item.premiere_date,
|
||||||
community_rating: item.community_rating,
|
community_rating: item.community_rating,
|
||||||
official_rating: item.official_rating,
|
official_rating: item.official_rating,
|
||||||
primary_image_tag: item.primary_image_tag,
|
primary_image_tag: item.primary_image_tag.clone(),
|
||||||
|
image_id: item.primary_image_tag,
|
||||||
backdrop_image_tags: item.backdrop_image_tags,
|
backdrop_image_tags: item.backdrop_image_tags,
|
||||||
parent_backdrop_image_tags: item.parent_backdrop_image_tags,
|
parent_backdrop_image_tags: item.parent_backdrop_image_tags,
|
||||||
album_id: item.album_id,
|
album_id: item.album_id,
|
||||||
@@ -1599,6 +1604,7 @@ impl MediaRepository for OfflineRepository {
|
|||||||
id: person_data.0,
|
id: person_data.0,
|
||||||
name: person_data.1,
|
name: person_data.1,
|
||||||
item_type: "Person".to_string(),
|
item_type: "Person".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Person,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: self.server_id.clone(),
|
server_id: self.server_id.clone(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
@@ -1606,11 +1612,13 @@ impl MediaRepository for OfflineRepository {
|
|||||||
overview: person_data.2,
|
overview: person_data.2,
|
||||||
genres: None,
|
genres: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
production_year: None,
|
production_year: None,
|
||||||
premiere_date: None,
|
premiere_date: None,
|
||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
primary_image_tag: person_data.3,
|
primary_image_tag: person_data.3.clone(),
|
||||||
|
image_id: person_data.3,
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
@@ -2100,6 +2108,7 @@ mod tests {
|
|||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
item_type: "Audio".to_string(),
|
item_type: "Audio".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Track,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: "test-server".to_string(),
|
server_id: "test-server".to_string(),
|
||||||
parent_id: parent_id.map(|s| s.to_string()),
|
parent_id: parent_id.map(|s| s.to_string()),
|
||||||
@@ -2107,11 +2116,13 @@ mod tests {
|
|||||||
overview: None,
|
overview: None,
|
||||||
genres: None,
|
genres: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
production_year: None,
|
production_year: None,
|
||||||
premiere_date: None,
|
premiere_date: None,
|
||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
|
|||||||
@@ -619,10 +619,13 @@ impl JellyfinItem {
|
|||||||
let primary_tag = self.image_tags.as_ref().and_then(|tags| tags.primary());
|
let primary_tag = self.image_tags.as_ref().and_then(|tags| tags.primary());
|
||||||
let backdrop_tags = self.backdrop_image_tags;
|
let backdrop_tags = self.backdrop_image_tags;
|
||||||
|
|
||||||
|
let kind = crate::domain::kind_from_jellyfin(&self.item_type, self.is_folder);
|
||||||
|
|
||||||
MediaItem {
|
MediaItem {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
name: self.name,
|
name: self.name,
|
||||||
item_type: self.item_type,
|
item_type: self.item_type,
|
||||||
|
kind,
|
||||||
is_folder: self.is_folder,
|
is_folder: self.is_folder,
|
||||||
server_id,
|
server_id,
|
||||||
parent_id: self.parent_id,
|
parent_id: self.parent_id,
|
||||||
@@ -634,7 +637,9 @@ impl JellyfinItem {
|
|||||||
community_rating: self.community_rating,
|
community_rating: self.community_rating,
|
||||||
official_rating: self.official_rating,
|
official_rating: self.official_rating,
|
||||||
runtime_ticks: self.run_time_ticks,
|
runtime_ticks: self.run_time_ticks,
|
||||||
primary_image_tag: primary_tag,
|
duration_ms: self.run_time_ticks.map(crate::domain::ticks_to_ms),
|
||||||
|
primary_image_tag: primary_tag.clone(),
|
||||||
|
image_id: primary_tag,
|
||||||
backdrop_image_tags: backdrop_tags,
|
backdrop_image_tags: backdrop_tags,
|
||||||
parent_backdrop_image_tags: self.parent_backdrop_image_tags,
|
parent_backdrop_image_tags: self.parent_backdrop_image_tags,
|
||||||
album_id: self.album_id,
|
album_id: self.album_id,
|
||||||
@@ -923,6 +928,7 @@ impl MediaRepository for OnlineRepository {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "Unknown Album".to_string()),
|
.unwrap_or_else(|| "Unknown Album".to_string()),
|
||||||
item_type: "MusicAlbum".to_string(),
|
item_type: "MusicAlbum".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Album,
|
||||||
is_folder: true,
|
is_folder: true,
|
||||||
server_id: first_track.server_id.clone(),
|
server_id: first_track.server_id.clone(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
@@ -934,7 +940,9 @@ impl MediaRepository for OnlineRepository {
|
|||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
primary_image_tag: first_track.primary_image_tag.clone(),
|
primary_image_tag: first_track.primary_image_tag.clone(),
|
||||||
|
image_id: first_track.primary_image_tag.clone(),
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
|
|||||||
@@ -97,13 +97,24 @@ pub struct Person {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Media item
|
/// Media item
|
||||||
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
|
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MediaItem {
|
pub struct MediaItem {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
|
||||||
|
///
|
||||||
|
/// Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
|
||||||
|
/// is the neutral replacement. This field stays while the frontend migrates
|
||||||
|
/// off it, then is removed in a later phase. New Rust code should read
|
||||||
|
/// `kind`, not this.
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub item_type: String,
|
pub item_type: String,
|
||||||
|
/// Provider-neutral classification — the replacement for `item_type`.
|
||||||
|
/// Populated by the Jellyfin mapping; defaults to `Other` for the handful of
|
||||||
|
/// construction sites that have not been migrated yet.
|
||||||
|
#[serde(default)]
|
||||||
|
pub kind: crate::domain::MediaKind,
|
||||||
/// Whether this item is a folder/container (vs a playable leaf). Used to
|
/// Whether this item is a folder/container (vs a playable leaf). Used to
|
||||||
/// decide whether a channel item drills into a list or plays directly.
|
/// decide whether a channel item drills into a list or plays directly.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -127,11 +138,25 @@ pub struct MediaItem {
|
|||||||
pub community_rating: Option<f64>,
|
pub community_rating: Option<f64>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub official_rating: Option<String>,
|
pub official_rating: Option<String>,
|
||||||
|
/// Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
|
||||||
|
/// `duration_ms`; dual-carried while the frontend migrates
|
||||||
|
/// (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "runTimeTicks")]
|
#[serde(rename = "runTimeTicks")]
|
||||||
pub runtime_ticks: Option<i64>,
|
pub runtime_ticks: Option<i64>,
|
||||||
|
/// Duration in milliseconds — the neutral replacement for `runtime_ticks`.
|
||||||
|
/// Ticks never reach the frontend; this does.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub duration_ms: Option<i64>,
|
||||||
|
/// Legacy Jellyfin primary image tag. Being replaced by `image_id`;
|
||||||
|
/// dual-carried while the frontend migrates. New code should read `image_id`.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub primary_image_tag: Option<String>,
|
pub primary_image_tag: Option<String>,
|
||||||
|
/// Neutral image identifier the frontend resolves to a URL via the image
|
||||||
|
/// command — the replacement for `primary_image_tag`. Same value today
|
||||||
|
/// (Jellyfin's tag is the id); the rename removes the provider term.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_id: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub backdrop_image_tags: Option<Vec<String>>,
|
pub backdrop_image_tags: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -525,6 +550,7 @@ mod tests {
|
|||||||
id: "1".to_string(),
|
id: "1".to_string(),
|
||||||
name: "Test".to_string(),
|
name: "Test".to_string(),
|
||||||
item_type: "Audio".to_string(),
|
item_type: "Audio".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Track,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: "server1".to_string(),
|
server_id: "server1".to_string(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
@@ -536,7 +562,9 @@ mod tests {
|
|||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
@@ -675,6 +703,7 @@ mod tests {
|
|||||||
id: "track1".to_string(),
|
id: "track1".to_string(),
|
||||||
name: "Test Track".to_string(),
|
name: "Test Track".to_string(),
|
||||||
item_type: "Audio".to_string(),
|
item_type: "Audio".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Track,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: "server1".to_string(),
|
server_id: "server1".to_string(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
@@ -686,7 +715,9 @@ mod tests {
|
|||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
@@ -736,6 +767,7 @@ mod tests {
|
|||||||
id: "1".to_string(),
|
id: "1".to_string(),
|
||||||
name: "Track".to_string(),
|
name: "Track".to_string(),
|
||||||
item_type: "Audio".to_string(),
|
item_type: "Audio".to_string(),
|
||||||
|
kind: crate::domain::MediaKind::Track,
|
||||||
is_folder: false,
|
is_folder: false,
|
||||||
server_id: "s1".to_string(),
|
server_id: "s1".to_string(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
@@ -747,7 +779,9 @@ mod tests {
|
|||||||
community_rating: None,
|
community_rating: None,
|
||||||
official_rating: None,
|
official_rating: None,
|
||||||
runtime_ticks: None,
|
runtime_ticks: None,
|
||||||
|
duration_ms: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
backdrop_image_tags: None,
|
backdrop_image_tags: None,
|
||||||
parent_backdrop_image_tags: None,
|
parent_backdrop_image_tags: None,
|
||||||
album_id: None,
|
album_id: None,
|
||||||
|
|||||||
+95
-4
@@ -1802,7 +1802,22 @@ export type LmsSyncGroup = { masterMac: string; masterName?: string; slaveMacs?:
|
|||||||
/**
|
/**
|
||||||
* Media item
|
* Media item
|
||||||
*/
|
*/
|
||||||
export type MediaItem = { id: string; name: string; type: string;
|
export type MediaItem = { id: string; name: string;
|
||||||
|
/**
|
||||||
|
* Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
|
||||||
|
*
|
||||||
|
* Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
|
||||||
|
* is the neutral replacement. This field stays while the frontend migrates
|
||||||
|
* off it, then is removed in a later phase. New Rust code should read
|
||||||
|
* `kind`, not this.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
/**
|
||||||
|
* Provider-neutral classification — the replacement for `item_type`.
|
||||||
|
* Populated by the Jellyfin mapping; defaults to `Other` for the handful of
|
||||||
|
* construction sites that have not been migrated yet.
|
||||||
|
*/
|
||||||
|
kind?: MediaKind;
|
||||||
/**
|
/**
|
||||||
* Whether this item is a folder/container (vs a playable leaf). Used to
|
* Whether this item is a folder/container (vs a playable leaf). Used to
|
||||||
* decide whether a channel item drills into a list or plays directly.
|
* decide whether a channel item drills into a list or plays directly.
|
||||||
@@ -1812,7 +1827,46 @@ isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: stri
|
|||||||
* ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
|
* ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
|
||||||
* podcast episodes by release date.
|
* podcast episodes by release date.
|
||||||
*/
|
*/
|
||||||
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
|
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null;
|
||||||
|
/**
|
||||||
|
* Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
|
||||||
|
* `duration_ms`; dual-carried while the frontend migrates
|
||||||
|
* (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
|
||||||
|
*/
|
||||||
|
runTimeTicks?: number | null;
|
||||||
|
/**
|
||||||
|
* Duration in milliseconds — the neutral replacement for `runtime_ticks`.
|
||||||
|
* Ticks never reach the frontend; this does.
|
||||||
|
*/
|
||||||
|
durationMs?: number | null;
|
||||||
|
/**
|
||||||
|
* Legacy Jellyfin primary image tag. Being replaced by `image_id`;
|
||||||
|
* dual-carried while the frontend migrates. New code should read `image_id`.
|
||||||
|
*/
|
||||||
|
primaryImageTag?: string | null;
|
||||||
|
/**
|
||||||
|
* Neutral image identifier the frontend resolves to a URL via the image
|
||||||
|
* command — the replacement for `primary_image_tag`. Same value today
|
||||||
|
* (Jellyfin's tag is the id); the rename removes the provider term.
|
||||||
|
*/
|
||||||
|
imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
|
||||||
|
/**
|
||||||
|
* The kind of a media item — provider-neutral classification.
|
||||||
|
*
|
||||||
|
* Replaces the stringly-typed `item_type` that carried Jellyfin's vocabulary
|
||||||
|
* (`"Audio"`, `"MusicAlbum"`, …) across the boundary. A closed enum means a
|
||||||
|
* typo or an unhandled kind is a compile error on the frontend, not a silent
|
||||||
|
* runtime miss across ~127 comparison sites.
|
||||||
|
*/
|
||||||
|
export type MediaKind = "track" | "album" | "artist" | "playlist" | "movie" | "series" | "season" | "episode" | "person" | "channel" | "folder" |
|
||||||
|
/**
|
||||||
|
* A kind we do not model explicitly. Reached only for provider item types
|
||||||
|
* that map to nothing meaningful; consumers treat it like an opaque
|
||||||
|
* container. The mapping must be *total* — it never panics — so this is the
|
||||||
|
* safe sink for unknown strings. Also the `Default`, so a defaulted
|
||||||
|
* `MediaItem` (see the dual-carry migration) is inert rather than a lie.
|
||||||
|
*/
|
||||||
|
"other"
|
||||||
/**
|
/**
|
||||||
* Media session type tracking the high-level playback context
|
* Media session type tracking the high-level playback context
|
||||||
*/
|
*/
|
||||||
@@ -2276,7 +2330,22 @@ export type PlaylistEntry =
|
|||||||
/**
|
/**
|
||||||
* The underlying media item
|
* The underlying media item
|
||||||
*/
|
*/
|
||||||
({ id: string; name: string; type: string;
|
({ id: string; name: string;
|
||||||
|
/**
|
||||||
|
* Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
|
||||||
|
*
|
||||||
|
* Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
|
||||||
|
* is the neutral replacement. This field stays while the frontend migrates
|
||||||
|
* off it, then is removed in a later phase. New Rust code should read
|
||||||
|
* `kind`, not this.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
/**
|
||||||
|
* Provider-neutral classification — the replacement for `item_type`.
|
||||||
|
* Populated by the Jellyfin mapping; defaults to `Other` for the handful of
|
||||||
|
* construction sites that have not been migrated yet.
|
||||||
|
*/
|
||||||
|
kind?: MediaKind;
|
||||||
/**
|
/**
|
||||||
* Whether this item is a folder/container (vs a playable leaf). Used to
|
* Whether this item is a folder/container (vs a playable leaf). Used to
|
||||||
* decide whether a channel item drills into a list or plays directly.
|
* decide whether a channel item drills into a list or plays directly.
|
||||||
@@ -2286,7 +2355,29 @@ isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: stri
|
|||||||
* ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
|
* ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
|
||||||
* podcast episodes by release date.
|
* podcast episodes by release date.
|
||||||
*/
|
*/
|
||||||
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
|
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null;
|
||||||
|
/**
|
||||||
|
* Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
|
||||||
|
* `duration_ms`; dual-carried while the frontend migrates
|
||||||
|
* (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
|
||||||
|
*/
|
||||||
|
runTimeTicks?: number | null;
|
||||||
|
/**
|
||||||
|
* Duration in milliseconds — the neutral replacement for `runtime_ticks`.
|
||||||
|
* Ticks never reach the frontend; this does.
|
||||||
|
*/
|
||||||
|
durationMs?: number | null;
|
||||||
|
/**
|
||||||
|
* Legacy Jellyfin primary image tag. Being replaced by `image_id`;
|
||||||
|
* dual-carried while the frontend migrates. New code should read `image_id`.
|
||||||
|
*/
|
||||||
|
primaryImageTag?: string | null;
|
||||||
|
/**
|
||||||
|
* Neutral image identifier the frontend resolves to a URL via the image
|
||||||
|
* command — the replacement for `primary_image_tag`. Same value today
|
||||||
|
* (Jellyfin's tag is the id); the rename removes the provider term.
|
||||||
|
*/
|
||||||
|
imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
|
||||||
/**
|
/**
|
||||||
* The playlist-scoped entry ID (Jellyfin's PlaylistItemId)
|
* The playlist-scoped entry ID (Jellyfin's PlaylistItemId)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user