Compare commits

...

33 Commits

Author SHA1 Message Date
maxid
6c98ec2fa0 migrate season files to episode files and drop legacy table 2026-02-22 16:19:27 +01:00
natarelli22
d8a0ec66c3 Support for handling Single Episode Torrents (#331)
**Description**
As explained on #322, MediaManager currently only matches torrents that
represent full seasons or season packs.
As a result, valid episode-based releases — commonly returned by
indexers such as EZTV — are filtered out during scoring and never
considered for download.

Initial changes to the season parsing logic allow these torrents to be
discovered.
However, additional changes are required beyond season parsing to
properly support single-episode imports.

This PR is intended as a work-in-progress / RFC to discuss the required
changes and align on the correct approach before completing the
implementation.

**Things planned to do**
[X] Update Web UI to better display episode-level details
[ ] Update TV show import logic to handle single episode files, instead
of assuming full season files (to avoid integrity errors when episodes
are missing)
[ ] Create episode file tables to store episode-level data, similar to
season files
[ ] Implement fetching and downloading logic for single-episode torrents

**Notes / current limitations**
At the moment, the database and import logic assume one file per season
per quality, which works for season packs but not for episode-based
releases.

These changes are intentionally not completed yet and are part of the
discussion this PR aims to start.

**Request for feedback**
This represents a significant change in how TV content is handled in
MediaManager.
Before proceeding further, feedback from @maxdorninger on the overall
direction and next steps would be greatly appreciated.

Once aligned, the remaining tasks can be implemented incrementally.

---------

Co-authored-by: Maximilian Dorninger <97409287+maxdorninger@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-22 15:21:19 +01:00
Maximilian Dorninger
094d0e4eb7 update logo
forgot to update this file
2026-02-21 21:48:37 +01:00
Maximilian Dorninger
4d7f596ffd Rebrand to new MediaManager logo (#452)
I made two new logos because the old one wasn't very recognizable at a
glance.


![1](https://github.com/user-attachments/assets/cb37a709-e80b-4c97-a4d8-cf9ba0dc1613)

![3](https://github.com/user-attachments/assets/c56ded5c-fe15-4c02-bc20-fe2bff06caf9)
2026-02-21 20:29:16 +01:00
dependabot[bot]
300df14c8c Bump svelte from 5.51.0 to 5.53.0 in /web in the npm_and_yarn group across 1 directory (#445)
Bumps the npm_and_yarn group with 1 update in the /web directory:
[svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte).

Updates `svelte` from 5.51.0 to 5.53.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sveltejs/svelte/releases">svelte's
releases</a>.</em></p>
<blockquote>
<h2>svelte@5.53.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p>feat: allow comments in tags (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17671">#17671</a>)</p>
</li>
<li>
<p>feat: allow error boundaries to work on the server (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17672">#17672</a>)</p>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p>fix: use TrustedHTML to test for customizable <!-- raw HTML omitted
--> support, where necessary (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17743">#17743</a>)</p>
</li>
<li>
<p>fix: ensure head effects are kept in the effect tree (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17746">#17746</a>)</p>
</li>
<li>
<p>chore: deactivate current_batch by default in unset_context (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17738">#17738</a>)</p>
</li>
</ul>
<h2>svelte@5.52.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>feat: support TrustedHTML in <code>{@html}</code> expressions (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17701">#17701</a>)</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p>fix: repair dynamic component truthy/falsy hydration mismatches (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17737">#17737</a>)</p>
</li>
<li>
<p>fix: re-run non-render-bound deriveds on the server (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17674">#17674</a>)</p>
</li>
</ul>
<h2>svelte@5.51.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p>fix: check to make sure <code>svelte:element</code> tags are valid
during SSR (<a
href="73098bb26c"><code>73098bb26c6f06e7fd1b0746d817d2c5ee90755f</code></a>)</p>
</li>
<li>
<p>fix: misc option escaping and backwards compatibility (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17741">#17741</a>)</p>
</li>
<li>
<p>fix: strip event handlers during SSR (<a
href="a0c7f28915"><code>a0c7f289156e9fafaeaf5ca14af6c06fe9b9eae5</code></a>)</p>
</li>
<li>
<p>fix: replace usage of <code>for in</code> with <code>for of
Object.keys</code> (<a
href="f89c7ddd7e"><code>f89c7ddd7eebaa1ef3cc540400bec2c9140b330c</code></a>)</p>
</li>
<li>
<p>fix: always escape option body in SSR (<a
href="f7c80da18c"><code>f7c80da18c215e3727c2a611b0b8744cc6e504c5</code></a>)</p>
</li>
<li>
<p>chore: upgrade <code>devalue</code> (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17739">#17739</a>)</p>
</li>
</ul>
<h2>svelte@5.51.4</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p>chore: proactively defer effects in pending boundary (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17734">#17734</a>)</p>
</li>
<li>
<p>fix: detect and error on non-idempotent each block keys in dev mode
(<a
href="https://redirect.github.com/sveltejs/svelte/pull/17732">#17732</a>)</p>
</li>
</ul>
<h2>svelte@5.51.3</h2>
<h3>Patch Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md">svelte's
changelog</a>.</em></p>
<blockquote>
<h2>5.53.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p>feat: allow comments in tags (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17671">#17671</a>)</p>
</li>
<li>
<p>feat: allow error boundaries to work on the server (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17672">#17672</a>)</p>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p>fix: use TrustedHTML to test for customizable
<code>&lt;select&gt;</code> support, where necessary (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17743">#17743</a>)</p>
</li>
<li>
<p>fix: ensure head effects are kept in the effect tree (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17746">#17746</a>)</p>
</li>
<li>
<p>chore: deactivate current_batch by default in unset_context (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17738">#17738</a>)</p>
</li>
</ul>
<h2>5.52.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>feat: support TrustedHTML in <code>{@html}</code> expressions (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17701">#17701</a>)</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p>fix: repair dynamic component truthy/falsy hydration mismatches (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17737">#17737</a>)</p>
</li>
<li>
<p>fix: re-run non-render-bound deriveds on the server (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17674">#17674</a>)</p>
</li>
</ul>
<h2>5.51.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p>fix: check to make sure <code>svelte:element</code> tags are valid
during SSR (<a
href="73098bb26c"><code>73098bb26c6f06e7fd1b0746d817d2c5ee90755f</code></a>)</p>
</li>
<li>
<p>fix: misc option escaping and backwards compatibility (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17741">#17741</a>)</p>
</li>
<li>
<p>fix: strip event handlers during SSR (<a
href="a0c7f28915"><code>a0c7f289156e9fafaeaf5ca14af6c06fe9b9eae5</code></a>)</p>
</li>
<li>
<p>fix: replace usage of <code>for in</code> with <code>for of
Object.keys</code> (<a
href="f89c7ddd7e"><code>f89c7ddd7eebaa1ef3cc540400bec2c9140b330c</code></a>)</p>
</li>
<li>
<p>fix: always escape option body in SSR (<a
href="f7c80da18c"><code>f7c80da18c215e3727c2a611b0b8744cc6e504c5</code></a>)</p>
</li>
<li>
<p>chore: upgrade <code>devalue</code> (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17739">#17739</a>)</p>
</li>
</ul>
<h2>5.51.4</h2>
<h3>Patch Changes</h3>
<ul>
<li>chore: proactively defer effects in pending boundary (<a
href="https://redirect.github.com/sveltejs/svelte/pull/17734">#17734</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c2fc95a467"><code>c2fc95a</code></a>
Version Packages (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17747">#17747</a>)</li>
<li><a
href="92e2fc1209"><code>92e2fc1</code></a>
feat: allow comments in tags (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17671">#17671</a>)</li>
<li><a
href="2661513cd3"><code>2661513</code></a>
feat: allow error boundaries to work on the server (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17672">#17672</a>)</li>
<li><a
href="582e4443dc"><code>582e444</code></a>
fix: ensure head effects are kept in the effect tree (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17746">#17746</a>)</li>
<li><a
href="f8bf9bb461"><code>f8bf9bb</code></a>
chore: deactivate current_batch by default in unset_context (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17738">#17738</a>)</li>
<li><a
href="696d97ff3e"><code>696d97f</code></a>
fix: use TrustedHTML to test for customizable &lt;select&gt; support,
where necessa...</li>
<li><a
href="cbf4e246fc"><code>cbf4e24</code></a>
Version Packages (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17742">#17742</a>)</li>
<li><a
href="09c4cb5084"><code>09c4cb5</code></a>
fix: re-run non-render-bound deriveds on the server (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17674">#17674</a>)</li>
<li><a
href="be24b0dca7"><code>be24b0d</code></a>
feat: support TrustedHTML in {<a
href="https://github.com/html"><code>@​html</code></a>} expressions (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17701">#17701</a>)</li>
<li><a
href="9f48e7620f"><code>9f48e76</code></a>
fix: repair dynamic component truthy/falsy hydration mismatches (<a
href="https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte/issues/17737">#17737</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/sveltejs/svelte/commits/svelte@5.53.0/packages/svelte">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=svelte&package-manager=npm_and_yarn&previous-version=5.51.0&new-version=5.53.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maxdorninger/MediaManager/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 18:08:06 +01:00
dependabot[bot]
2f102d6c5d Bump the uv group across 1 directory with 2 updates (#446)
Bumps the uv group with 2 updates in the /metadata_relay directory:
[python-multipart](https://github.com/Kludex/python-multipart) and
[urllib3](https://github.com/urllib3/urllib3).

Updates `python-multipart` from 0.0.21 to 0.0.22
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/python-multipart/releases">python-multipart's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.0.22</h2>
<h2>What's Changed</h2>
<ul>
<li>Drop directory path from filename in <code>File</code> <a
href="9433f4bbc9">9433f4b</a>.</li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22">https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md">python-multipart's
changelog</a>.</em></p>
<blockquote>
<h2>0.0.22 (2026-01-25)</h2>
<ul>
<li>Drop directory path from filename in <code>File</code> <a
href="9433f4bbc9">9433f4b</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bea7bbb290"><code>bea7bbb</code></a>
Version 0.0.22 (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/222">#222</a>)</li>
<li><a
href="0fb59a9df0"><code>0fb59a9</code></a>
chore: add return type on test (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/221">#221</a>)</li>
<li><a
href="9433f4bbc9"><code>9433f4b</code></a>
Merge commit from fork</li>
<li><a
href="d5c91ecb0a"><code>d5c91ec</code></a>
Bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/219">#219</a>)</li>
<li><a
href="5a90631b48"><code>5a90631</code></a>
bump uv (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/218">#218</a>)</li>
<li>See full diff in <a
href="https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22">compare
view</a></li>
</ul>
</details>
<br />

Updates `urllib3` from 2.6.2 to 2.6.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.6.3</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Changes</h2>
<ul>
<li>Fixed a security issue where decompression-bomb safeguards of the
streaming API were bypassed when HTTP redirects were followed.
(CVE-2026-21441 reported by <a
href="https://github.com/D47A"><code>@​D47A</code></a>, 8.9 High,
GHSA-38jv-5279-wg99)</li>
<li>Started treating <code>Retry-After</code> times greater than 6 hours
as 6 hours by default. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3743">urllib3/urllib3#3743</a>)</li>
<li>Fixed <code>urllib3.connection.VerifiedHTTPSConnection</code> on
Emscripten. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3752">urllib3/urllib3#3752</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.6.3 (2026-01-07)</h1>
<ul>
<li>Fixed a high-severity security issue where decompression-bomb
safeguards of
the streaming API were bypassed when HTTP redirects were followed.
(<code>GHSA-38jv-5279-wg99
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-38jv-5279-wg99&gt;</code>__)</li>
<li>Started treating <code>Retry-After</code> times greater than 6 hours
as 6 hours by
default. (<code>[#3743](https://github.com/urllib3/urllib3/issues/3743)
&lt;https://github.com/urllib3/urllib3/issues/3743&gt;</code>__)</li>
<li>Fixed <code>urllib3.connection.VerifiedHTTPSConnection</code> on
Emscripten.
(<code>[#3752](https://github.com/urllib3/urllib3/issues/3752)
&lt;https://github.com/urllib3/urllib3/issues/3752&gt;</code>__)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0248277dd7"><code>0248277</code></a>
Release 2.6.3</li>
<li><a
href="8864ac407b"><code>8864ac4</code></a>
Merge commit from fork</li>
<li><a
href="70cecb27ca"><code>70cecb2</code></a>
Fix Scorecard issues related to vulnerable dev dependencies (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3755">#3755</a>)</li>
<li><a
href="41f249abe1"><code>41f249a</code></a>
Move &quot;v2.0 Migration Guide&quot; to the end of the table of
contents (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3747">#3747</a>)</li>
<li><a
href="fd4dffd2fc"><code>fd4dffd</code></a>
Patch <code>VerifiedHTTPSConnection</code> for Emscripten (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3752">#3752</a>)</li>
<li><a
href="13f0bfd55e"><code>13f0bfd</code></a>
Handle massive values in Retry-After when calculating time to sleep for
(<a
href="https://redirect.github.com/urllib3/urllib3/issues/3743">#3743</a>)</li>
<li><a
href="8c480bf87b"><code>8c480bf</code></a>
Bump actions/upload-artifact from 5.0.0 to 6.0.0 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3748">#3748</a>)</li>
<li><a
href="4b40616e95"><code>4b40616</code></a>
Bump actions/cache from 4.3.0 to 5.0.1 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3750">#3750</a>)</li>
<li><a
href="82b8479663"><code>82b8479</code></a>
Bump actions/download-artifact from 6.0.0 to 7.0.0 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3749">#3749</a>)</li>
<li><a
href="34284cb017"><code>34284cb</code></a>
Mention experimental features in the security policy (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3746">#3746</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.6.2...2.6.3">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maxdorninger/MediaManager/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 18:07:53 +01:00
dependabot[bot]
3e696c463c Bump the uv group across 1 directory with 3 updates (#448)
Bumps the uv group with 3 updates in the / directory:
[cryptography](https://github.com/pyca/cryptography),
[python-multipart](https://github.com/Kludex/python-multipart) and
[urllib3](https://github.com/urllib3/urllib3).

Updates `cryptography` from 46.0.3 to 46.0.5
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst">cryptography's
changelog</a>.</em></p>
<blockquote>
<p>46.0.5 - 2026-02-10</p>
<pre><code>
* An attacker could create a malicious public key that reveals portions
of your
private key when using certain uncommon elliptic curves (binary curves).
This version now includes additional security checks to prevent this
attack.
This issue only affects binary elliptic curves, which are rarely used in
real-world applications. Credit to **XlabAI Team of Tencent Xuanwu Lab
and
Atuin Automated Vulnerability Discovery Engine** for reporting the
issue.
  **CVE-2026-26007**
* Support for ``SECT*`` binary elliptic curves is deprecated and will be
  removed in the next release.
<p>.. v46-0-4:</p>
<p>46.0.4 - 2026-01-27<br />
</code></pre></p>
<ul>
<li><code>Dropped support for win_arm64 wheels</code>_.</li>
<li>Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
3.5.5.</li>
</ul>
<p>.. _v46-0-3:</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="06e120e682"><code>06e120e</code></a>
bump version for 46.0.5 release (<a
href="https://redirect.github.com/pyca/cryptography/issues/14289">#14289</a>)</li>
<li><a
href="0eebb9dbb6"><code>0eebb9d</code></a>
EC check key on cofactor &gt; 1 (<a
href="https://redirect.github.com/pyca/cryptography/issues/14287">#14287</a>)</li>
<li><a
href="bedf6e186b"><code>bedf6e1</code></a>
fix openssl version on 46 branch (<a
href="https://redirect.github.com/pyca/cryptography/issues/14220">#14220</a>)</li>
<li><a
href="e6f44fc8e6"><code>e6f44fc</code></a>
bump for 46.0.4 and drop win arm64 due to CI issues (<a
href="https://redirect.github.com/pyca/cryptography/issues/14217">#14217</a>)</li>
<li>See full diff in <a
href="https://github.com/pyca/cryptography/compare/46.0.3...46.0.5">compare
view</a></li>
</ul>
</details>
<br />

Updates `python-multipart` from 0.0.21 to 0.0.22
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/python-multipart/releases">python-multipart's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.0.22</h2>
<h2>What's Changed</h2>
<ul>
<li>Drop directory path from filename in <code>File</code> <a
href="9433f4bbc9">9433f4b</a>.</li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22">https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md">python-multipart's
changelog</a>.</em></p>
<blockquote>
<h2>0.0.22 (2026-01-25)</h2>
<ul>
<li>Drop directory path from filename in <code>File</code> <a
href="9433f4bbc9">9433f4b</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bea7bbb290"><code>bea7bbb</code></a>
Version 0.0.22 (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/222">#222</a>)</li>
<li><a
href="0fb59a9df0"><code>0fb59a9</code></a>
chore: add return type on test (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/221">#221</a>)</li>
<li><a
href="9433f4bbc9"><code>9433f4b</code></a>
Merge commit from fork</li>
<li><a
href="d5c91ecb0a"><code>d5c91ec</code></a>
Bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/219">#219</a>)</li>
<li><a
href="5a90631b48"><code>5a90631</code></a>
bump uv (<a
href="https://redirect.github.com/Kludex/python-multipart/issues/218">#218</a>)</li>
<li>See full diff in <a
href="https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22">compare
view</a></li>
</ul>
</details>
<br />

Updates `urllib3` from 2.6.2 to 2.6.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.6.3</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Changes</h2>
<ul>
<li>Fixed a security issue where decompression-bomb safeguards of the
streaming API were bypassed when HTTP redirects were followed.
(CVE-2026-21441 reported by <a
href="https://github.com/D47A"><code>@​D47A</code></a>, 8.9 High,
GHSA-38jv-5279-wg99)</li>
<li>Started treating <code>Retry-After</code> times greater than 6 hours
as 6 hours by default. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3743">urllib3/urllib3#3743</a>)</li>
<li>Fixed <code>urllib3.connection.VerifiedHTTPSConnection</code> on
Emscripten. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3752">urllib3/urllib3#3752</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.6.3 (2026-01-07)</h1>
<ul>
<li>Fixed a high-severity security issue where decompression-bomb
safeguards of
the streaming API were bypassed when HTTP redirects were followed.
(<code>GHSA-38jv-5279-wg99
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-38jv-5279-wg99&gt;</code>__)</li>
<li>Started treating <code>Retry-After</code> times greater than 6 hours
as 6 hours by
default. (<code>[#3743](https://github.com/urllib3/urllib3/issues/3743)
&lt;https://github.com/urllib3/urllib3/issues/3743&gt;</code>__)</li>
<li>Fixed <code>urllib3.connection.VerifiedHTTPSConnection</code> on
Emscripten.
(<code>[#3752](https://github.com/urllib3/urllib3/issues/3752)
&lt;https://github.com/urllib3/urllib3/issues/3752&gt;</code>__)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0248277dd7"><code>0248277</code></a>
Release 2.6.3</li>
<li><a
href="8864ac407b"><code>8864ac4</code></a>
Merge commit from fork</li>
<li><a
href="70cecb27ca"><code>70cecb2</code></a>
Fix Scorecard issues related to vulnerable dev dependencies (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3755">#3755</a>)</li>
<li><a
href="41f249abe1"><code>41f249a</code></a>
Move &quot;v2.0 Migration Guide&quot; to the end of the table of
contents (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3747">#3747</a>)</li>
<li><a
href="fd4dffd2fc"><code>fd4dffd</code></a>
Patch <code>VerifiedHTTPSConnection</code> for Emscripten (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3752">#3752</a>)</li>
<li><a
href="13f0bfd55e"><code>13f0bfd</code></a>
Handle massive values in Retry-After when calculating time to sleep for
(<a
href="https://redirect.github.com/urllib3/urllib3/issues/3743">#3743</a>)</li>
<li><a
href="8c480bf87b"><code>8c480bf</code></a>
Bump actions/upload-artifact from 5.0.0 to 6.0.0 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3748">#3748</a>)</li>
<li><a
href="4b40616e95"><code>4b40616</code></a>
Bump actions/cache from 4.3.0 to 5.0.1 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3750">#3750</a>)</li>
<li><a
href="82b8479663"><code>82b8479</code></a>
Bump actions/download-artifact from 6.0.0 to 7.0.0 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3749">#3749</a>)</li>
<li><a
href="34284cb017"><code>34284cb</code></a>
Mention experimental features in the security policy (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3746">#3746</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.6.2...2.6.3">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maxdorninger/MediaManager/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 18:07:39 +01:00
dependabot[bot]
5adb88f9e0 Bump pillow from 12.1.0 to 12.1.1 (#443)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.1.0 to
12.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/python-pillow/Pillow/releases">pillow's
releases</a>.</em></p>
<blockquote>
<h2>12.1.1</h2>
<p><a
href="https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html">https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html</a></p>
<h2>Dependencies</h2>
<ul>
<li>Patch libavif for svt-av1 4.0 compatibility <a
href="https://redirect.github.com/python-pillow/Pillow/issues/9413">#9413</a>
[<a href="https://github.com/hugovk"><code>@​hugovk</code></a>]</li>
</ul>
<h2>Other changes</h2>
<ul>
<li>Fix OOB Write with invalid tile extents <a
href="https://redirect.github.com/python-pillow/Pillow/issues/9427">#9427</a>
[<a
href="https://github.com/radarhere"><code>@​radarhere</code></a>]</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5158d98c80"><code>5158d98</code></a>
12.1.1 version bump</li>
<li><a
href="9000313cc5"><code>9000313</code></a>
Fix OOB Write with invalid tile extents (<a
href="https://redirect.github.com/python-pillow/Pillow/issues/9427">#9427</a>)</li>
<li><a
href="cd0111849f"><code>cd01118</code></a>
Patch libavif for svt-av1 4.0 compatibility</li>
<li>See full diff in <a
href="https://github.com/python-pillow/Pillow/compare/12.1.0...12.1.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pillow&package-manager=uv&previous-version=12.1.0&new-version=12.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:35:11 +01:00
dependabot[bot]
eb277dddac Bump sveltekit-superforms from 2.28.1 to 2.29.1 in /web (#442)
Bumps
[sveltekit-superforms](https://github.com/ciscoheat/sveltekit-superforms)
from 2.28.1 to 2.29.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ciscoheat/sveltekit-superforms/releases">sveltekit-superforms's
releases</a>.</em></p>
<blockquote>
<h2>v2.29.1</h2>
<h3>Fixed</h3>
<ul>
<li>Fixed TypeScript type inference for discriminated unions in
<code>ValidationErrors</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/653">#653</a></li>
<li>Fixed FormData parsing for discriminated unions, so they work
properly without requiring <code>dataType: 'json'</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/655">#655</a></li>
<li><code>reset()</code> function didn't preserve tainted state for
fields that are not being reset when using partial data. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/656">#656</a></li>
<li>Fixed FormData parsing incorrectly coercing empty strings to literal
values (e.g., <code>z.literal(&quot;bar&quot;)</code>). Empty strings
now properly fail validation instead of being replaced with the literal
value. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/664">#664</a></li>
<li>Fixed <code>ReferenceError</code> when using
<code>customValidity</code> with <code>validateForm({ update: true
})</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/669">#669</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Replaced deprecated <code>@finom/zod-to-json-schema</code> with
<code>zod-v3-to-json-schema</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/pull/660">#660</a></li>
<li>Migrated Valibot adapter to use the official
<code>@valibot/to-json-schema</code> package. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/pull/668">#668</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ciscoheat/sveltekit-superforms/blob/main/CHANGELOG.md">sveltekit-superforms's
changelog</a>.</em></p>
<blockquote>
<h2>[2.29.1] - 2025-12-16</h2>
<h3>Fixed</h3>
<ul>
<li>Fixed TypeScript type inference for discriminated unions in
<code>ValidationErrors</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/653">#653</a></li>
<li>Fixed FormData parsing for discriminated unions, so they work
properly without requiring <code>dataType: 'json'</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/655">#655</a></li>
<li><code>reset()</code> function didn't preserve tainted state for
fields that are not being reset when using partial data. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/656">#656</a></li>
<li>Fixed FormData parsing incorrectly coercing empty strings to literal
values (e.g., <code>z.literal(&quot;bar&quot;)</code>). Empty strings
now properly fail validation instead of being replaced with the literal
value. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/664">#664</a></li>
<li>Fixed <code>ReferenceError</code> when using
<code>customValidity</code> with <code>validateForm({ update: true
})</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/issues/669">#669</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Replaced deprecated <code>@finom/zod-to-json-schema</code> with
<code>zod-v3-to-json-schema</code>. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/pull/660">#660</a></li>
<li>Migrated Valibot adapter to use the official
<code>@valibot/to-json-schema</code> package. <a
href="https://redirect.github.com/ciscoheat/sveltekit-superforms/pull/668">#668</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="22319df44e"><code>22319df</code></a>
2.29.1 changelog</li>
<li><a
href="c1b44308a8"><code>c1b4430</code></a>
2.29.1</li>
<li><a
href="9408124dc9"><code>9408124</code></a>
Incorrect dependency fix</li>
<li><a
href="887aeb6238"><code>887aeb6</code></a>
2.29.0</li>
<li><a
href="ce701fb6fc"><code>ce701fb</code></a>
2.29.0 changelog</li>
<li><a
href="58b41b1e84"><code>58b41b1</code></a>
Linter</li>
<li><a
href="fbbdb90ae7"><code>fbbdb90</code></a>
Fixed build warning</li>
<li><a
href="44a40c6b13"><code>44a40c6</code></a>
Fixed SvelteKit reference warnings</li>
<li><a
href="12bb4d5c32"><code>12bb4d5</code></a>
Using pnpm 10 for build</li>
<li><a
href="f7c87d8898"><code>f7c87d8</code></a>
Package updates</li>
<li>Additional commits viewable in <a
href="https://github.com/ciscoheat/sveltekit-superforms/compare/v2.28.1...v2.29.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sveltekit-superforms&package-manager=npm_and_yarn&previous-version=2.28.1&new-version=2.29.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:35:00 +01:00
dependabot[bot]
516d562bd8 Bump uvicorn from 0.40.0 to 0.41.0 (#441)
Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.40.0 to
0.41.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/uvicorn/releases">uvicorn's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.41.0</h2>
<h2>Added</h2>
<ul>
<li>Add <code>--limit-max-requests-jitter</code> to stagger worker
restarts (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2707">#2707</a>)</li>
<li>Add socket path to <code>scope[&quot;server&quot;]</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2561">#2561</a>)</li>
</ul>
<h2>Changed</h2>
<ul>
<li>Rename <code>LifespanOn.error_occured</code> to
<code>error_occurred</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2776">#2776</a>)</li>
</ul>
<h2>Fixed</h2>
<ul>
<li>Ignore permission denied errors in watchfiles reloader (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2817">#2817</a>)</li>
<li>Ensure lifespan shutdown runs when <code>should_exit</code> is set
during startup (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2812">#2812</a>)</li>
<li>Reduce the log level of 'request limit exceeded' messages (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2788">#2788</a>)</li>
</ul>
<hr />
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/t-kawasumi"><code>@​t-kawasumi</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2776">Kludex/uvicorn#2776</a></li>
<li><a href="https://github.com/fardyn"><code>@​fardyn</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2800">Kludex/uvicorn#2800</a></li>
<li><a href="https://github.com/ewie"><code>@​ewie</code></a> made their
first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2807">Kludex/uvicorn#2807</a></li>
<li><a href="https://github.com/shevron"><code>@​shevron</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2788">Kludex/uvicorn#2788</a></li>
<li><a href="https://github.com/jonashaag"><code>@​jonashaag</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2707">Kludex/uvicorn#2707</a></li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0">https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md">uvicorn's
changelog</a>.</em></p>
<blockquote>
<h2>0.41.0 (February 16, 2026)</h2>
<h3>Added</h3>
<ul>
<li>Add <code>--limit-max-requests-jitter</code> to stagger worker
restarts (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2707">#2707</a>)</li>
<li>Add socket path to <code>scope[&quot;server&quot;]</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2561">#2561</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Rename <code>LifespanOn.error_occured</code> to
<code>error_occurred</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2776">#2776</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Ignore permission denied errors in watchfiles reloader (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2817">#2817</a>)</li>
<li>Ensure lifespan shutdown runs when <code>should_exit</code> is set
during startup (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2812">#2812</a>)</li>
<li>Reduce the log level of 'request limit exceeded' messages (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2788">#2788</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9283c0f15c"><code>9283c0f</code></a>
Version 0.41.0 (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2821">#2821</a>)</li>
<li><a
href="a01a33eb8f"><code>a01a33e</code></a>
Add <code>--limit-max-requests-jitter</code> to stagger worker restarts
(<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2707">#2707</a>)</li>
<li><a
href="2ce65bde15"><code>2ce65bd</code></a>
Ignore permission denied errors in watchfiles reloader (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2817">#2817</a>)</li>
<li><a
href="654f2ed7d7"><code>654f2ed</code></a>
Ensure lifespan shutdown runs when <code>should_exit</code> is set
during startup (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2812">#2812</a>)</li>
<li><a
href="a03d9f6f0e"><code>a03d9f6</code></a>
Reduce the log level of 'request limit exceeded' messages (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2788">#2788</a>)</li>
<li><a
href="e377de40d0"><code>e377de4</code></a>
Add socket path to scope[&quot;server&quot;] (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2561">#2561</a>)</li>
<li><a
href="0779f7f8a4"><code>0779f7f</code></a>
Poll for readiness in <code>test_multiprocess_health_check</code> and
<code>run_server</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2816">#2816</a>)</li>
<li><a
href="7e9ce2c974"><code>7e9ce2c</code></a>
Poll for PID changes in <code>test_multiprocess_sighup</code> instead of
fixed sleep (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2">#2</a>...</li>
<li><a
href="99f0d8734d"><code>99f0d87</code></a>
Fix grep warning in scripts/sync-version (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2807">#2807</a>)</li>
<li><a
href="7ae2e6375a"><code>7ae2e63</code></a>
chore(deps): bump the python-packages group with 18 updates (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2801">#2801</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=uvicorn&package-manager=uv&previous-version=0.40.0&new-version=0.41.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:34:48 +01:00
dependabot[bot]
dea75841b2 Bump lucide-svelte from 0.564.0 to 0.574.0 in /web (#439)
Bumps
[lucide-svelte](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-svelte)
from 0.564.0 to 0.574.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/lucide-icons/lucide/releases">lucide-svelte's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.574.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(icons): changed <code>rocking-chair</code> icon by <a
href="https://github.com/jamiemlaw"><code>@​jamiemlaw</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3445">lucide-icons/lucide#3445</a></li>
<li>fix(icons): flipped <code>coins</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3158">lucide-icons/lucide#3158</a></li>
<li>feat(icons): added <code>x-line-top</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2838">lucide-icons/lucide#2838</a></li>
<li>feat(icons): added <code>mouse-left</code> icon by <a
href="https://github.com/marvfash"><code>@​marvfash</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2788">lucide-icons/lucide#2788</a></li>
<li>feat(icons): added <code>mouse-right</code> icon by <a
href="https://github.com/marvfash"><code>@​marvfash</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2787">lucide-icons/lucide#2787</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/marvfash"><code>@​marvfash</code></a>
made their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2788">lucide-icons/lucide#2788</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/0.572.0...0.574.0">https://github.com/lucide-icons/lucide/compare/0.572.0...0.574.0</a></p>
<h2>Version 0.573.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(icons): changed <code>rocking-chair</code> icon by <a
href="https://github.com/jamiemlaw"><code>@​jamiemlaw</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3445">lucide-icons/lucide#3445</a></li>
<li>fix(icons): flipped <code>coins</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3158">lucide-icons/lucide#3158</a></li>
<li>feat(icons): added <code>x-line-top</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2838">lucide-icons/lucide#2838</a></li>
<li>feat(icons): added <code>mouse-left</code> icon by <a
href="https://github.com/marvfash"><code>@​marvfash</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2788">lucide-icons/lucide#2788</a></li>
<li>feat(icons): added <code>mouse-right</code> icon by <a
href="https://github.com/marvfash"><code>@​marvfash</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2787">lucide-icons/lucide#2787</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/marvfash"><code>@​marvfash</code></a>
made their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/2788">lucide-icons/lucide#2788</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/0.572.0...0.573.0">https://github.com/lucide-icons/lucide/compare/0.572.0...0.573.0</a></p>
<h2>Version 0.572.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(icons): added <code>message-circle-check</code> icon by <a
href="https://github.com/Shrinks99"><code>@​Shrinks99</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3770">lucide-icons/lucide#3770</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Shrinks99"><code>@​Shrinks99</code></a>
made their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3770">lucide-icons/lucide#3770</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/0.571.0...0.572.0">https://github.com/lucide-icons/lucide/compare/0.571.0...0.572.0</a></p>
<h2>Version 0.571.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(icons): rearange <code>circle</code>-icons path and circle order
by <a
href="https://github.com/adamlindqvist"><code>@​adamlindqvist</code></a>
in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3746">lucide-icons/lucide#3746</a></li>
<li>feat(icons): added <code>shelving-unit</code> icon by <a
href="https://github.com/karsa-mistmere"><code>@​karsa-mistmere</code></a>
in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3041">lucide-icons/lucide#3041</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/adamlindqvist"><code>@​adamlindqvist</code></a>
made their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3746">lucide-icons/lucide#3746</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/0.570.0...0.571.0">https://github.com/lucide-icons/lucide/compare/0.570.0...0.571.0</a></p>
<h2>Version 0.570.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(icons): added <code>towel-rack</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3350">lucide-icons/lucide#3350</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/lucide-icons/lucide/commits/0.574.0/packages/lucide-svelte">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=lucide-svelte&package-manager=npm_and_yarn&previous-version=0.564.0&new-version=0.574.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:34:38 +01:00
dependabot[bot]
20e0dbf936 Bump uvicorn from 0.40.0 to 0.41.0 in /metadata_relay (#440)
Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.40.0 to
0.41.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/uvicorn/releases">uvicorn's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.41.0</h2>
<h2>Added</h2>
<ul>
<li>Add <code>--limit-max-requests-jitter</code> to stagger worker
restarts (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2707">#2707</a>)</li>
<li>Add socket path to <code>scope[&quot;server&quot;]</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2561">#2561</a>)</li>
</ul>
<h2>Changed</h2>
<ul>
<li>Rename <code>LifespanOn.error_occured</code> to
<code>error_occurred</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2776">#2776</a>)</li>
</ul>
<h2>Fixed</h2>
<ul>
<li>Ignore permission denied errors in watchfiles reloader (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2817">#2817</a>)</li>
<li>Ensure lifespan shutdown runs when <code>should_exit</code> is set
during startup (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2812">#2812</a>)</li>
<li>Reduce the log level of 'request limit exceeded' messages (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2788">#2788</a>)</li>
</ul>
<hr />
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/t-kawasumi"><code>@​t-kawasumi</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2776">Kludex/uvicorn#2776</a></li>
<li><a href="https://github.com/fardyn"><code>@​fardyn</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2800">Kludex/uvicorn#2800</a></li>
<li><a href="https://github.com/ewie"><code>@​ewie</code></a> made their
first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2807">Kludex/uvicorn#2807</a></li>
<li><a href="https://github.com/shevron"><code>@​shevron</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2788">Kludex/uvicorn#2788</a></li>
<li><a href="https://github.com/jonashaag"><code>@​jonashaag</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/uvicorn/pull/2707">Kludex/uvicorn#2707</a></li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0">https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md">uvicorn's
changelog</a>.</em></p>
<blockquote>
<h2>0.41.0 (February 16, 2026)</h2>
<h3>Added</h3>
<ul>
<li>Add <code>--limit-max-requests-jitter</code> to stagger worker
restarts (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2707">#2707</a>)</li>
<li>Add socket path to <code>scope[&quot;server&quot;]</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2561">#2561</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Rename <code>LifespanOn.error_occured</code> to
<code>error_occurred</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2776">#2776</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Ignore permission denied errors in watchfiles reloader (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2817">#2817</a>)</li>
<li>Ensure lifespan shutdown runs when <code>should_exit</code> is set
during startup (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2812">#2812</a>)</li>
<li>Reduce the log level of 'request limit exceeded' messages (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2788">#2788</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9283c0f15c"><code>9283c0f</code></a>
Version 0.41.0 (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2821">#2821</a>)</li>
<li><a
href="a01a33eb8f"><code>a01a33e</code></a>
Add <code>--limit-max-requests-jitter</code> to stagger worker restarts
(<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2707">#2707</a>)</li>
<li><a
href="2ce65bde15"><code>2ce65bd</code></a>
Ignore permission denied errors in watchfiles reloader (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2817">#2817</a>)</li>
<li><a
href="654f2ed7d7"><code>654f2ed</code></a>
Ensure lifespan shutdown runs when <code>should_exit</code> is set
during startup (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2812">#2812</a>)</li>
<li><a
href="a03d9f6f0e"><code>a03d9f6</code></a>
Reduce the log level of 'request limit exceeded' messages (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2788">#2788</a>)</li>
<li><a
href="e377de40d0"><code>e377de4</code></a>
Add socket path to scope[&quot;server&quot;] (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2561">#2561</a>)</li>
<li><a
href="0779f7f8a4"><code>0779f7f</code></a>
Poll for readiness in <code>test_multiprocess_health_check</code> and
<code>run_server</code> (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2816">#2816</a>)</li>
<li><a
href="7e9ce2c974"><code>7e9ce2c</code></a>
Poll for PID changes in <code>test_multiprocess_sighup</code> instead of
fixed sleep (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2">#2</a>...</li>
<li><a
href="99f0d8734d"><code>99f0d87</code></a>
Fix grep warning in scripts/sync-version (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2807">#2807</a>)</li>
<li><a
href="7ae2e6375a"><code>7ae2e63</code></a>
chore(deps): bump the python-packages group with 18 updates (<a
href="https://redirect.github.com/Kludex/uvicorn/issues/2801">#2801</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=uvicorn&package-manager=uv&previous-version=0.40.0&new-version=0.41.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:34:27 +01:00
dependabot[bot]
c8f2a4316e Bump alembic from 1.17.2 to 1.18.4 (#438)
Bumps [alembic](https://github.com/sqlalchemy/alembic) from 1.17.2 to
1.18.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sqlalchemy/alembic/releases">alembic's
releases</a>.</em></p>
<blockquote>
<h1>1.18.4</h1>
<p>Released: February 10, 2026</p>
<h2>bug</h2>
<ul>
<li>
<p><strong>[bug] [operations]</strong> Reverted the behavior of
<code>Operations.add_column()</code> that would
automatically render the &quot;PRIMARY KEY&quot; keyword inline when a
<code>Column</code> with <code>primary_key=True</code> is added. The
automatic
behavior, added in version 1.18.2, is now opt-in via the new
<code>Operations.add_column.inline_primary_key</code> parameter. This
change restores the ability to render a PostgreSQL SERIAL column, which
is
required to be <code>primary_key=True</code>, while not impacting the
ability to
render a separate primary key constraint. This also provides consistency
with the <code>Operations.add_column.inline_references</code> parameter
and
gives users explicit control over SQL generation.</p>
<p>To render PRIMARY KEY inline, use the
<code>Operations.add_column.inline_primary_key</code> parameter set to
<code>True</code>:</p>
<p>op.add_column(
&quot;my_table&quot;,
Column(&quot;id&quot;, Integer, primary_key=True),
inline_primary_key=True
)References: <a
href="https://redirect.github.com/sqlalchemy/alembic/issues/1232">#1232</a></p>
</li>
</ul>
<h1>1.18.3</h1>
<p>Released: January 29, 2026</p>
<h2>bug</h2>
<ul>
<li>
<p><strong>[bug] [autogenerate]</strong> Fixed regression in version
1.18.0 due to <a
href="https://redirect.github.com/sqlalchemy/alembic/issues/1771">#1771</a>
where autogenerate
would raise <code>NoReferencedTableError</code> when a foreign key
constraint
referenced a table that was not part of the initial table load,
including
tables filtered out by the
<code>EnvironmentContext.configure.include_name</code> callable or
tables
in remote schemas that were not included in the initial reflection
run.</p>
<p>The change in <a
href="https://redirect.github.com/sqlalchemy/alembic/issues/1771">#1771</a>
was a performance optimization that eliminated
additional reflection queries for tables that were only referenced by
foreign keys but not explicitly included in the main reflection run.
However, this optimization inadvertently removed the creation of
<code>Table</code> objects for these referenced tables, causing
autogenerate
to fail when processing foreign key constraints that pointed to
them.</p>
<p>The fix creates placeholder <code>Table</code> objects for foreign
key targets</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/sqlalchemy/alembic/commits">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=alembic&package-manager=uv&previous-version=1.17.2&new-version=1.18.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:34:15 +01:00
dependabot[bot]
4836e3e188 Bump prettier-plugin-svelte from 3.4.0 to 3.4.1 in /web (#437)
Bumps
[prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte)
from 3.4.0 to 3.4.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sveltejs/prettier-plugin-svelte/blob/v3.4.1/CHANGELOG.md">prettier-plugin-svelte's
changelog</a>.</em></p>
<blockquote>
<h2>3.4.1</h2>
<ul>
<li>(fix) externalize all prettier imports</li>
<li>(fix) don't remove parantheses of <code>bind:</code>ings with
<code>as</code> type casts</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/sveltejs/prettier-plugin-svelte/commits/v3.4.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=prettier-plugin-svelte&package-manager=npm_and_yarn&previous-version=3.4.0&new-version=3.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:33:54 +01:00
dependabot[bot]
7a6466ea9d Bump @sinclair/typebox from 0.34.41 to 0.34.48 in /web (#436)
Bumps
[@sinclair/typebox](https://github.com/sinclairzx81/typebox-legacy) from
0.34.41 to 0.34.48.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/sinclairzx81/typebox-legacy/commits/0.34.48">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for <code>@​sinclair/typebox</code> since your
current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sinclair/typebox&package-manager=npm_and_yarn&previous-version=0.34.41&new-version=0.34.48)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:33:33 +01:00
Maximilian Dorninger
b427aa5723 Merge pull request #433 from maxdorninger/dependabot/npm_and_yarn/web/npm_and_yarn-e0ba90b5b1
Bump @sveltejs/kit from 2.49.2 to 2.51.0 in /web in the npm_and_yarn group across 1 directory
2026-02-13 19:39:57 +01:00
dependabot[bot]
82aa01a650 Bump @sveltejs/kit in /web in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the /web directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit).


Updates `@sveltejs/kit` from 2.49.2 to 2.51.0
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.51.0/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.51.0
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:25:01 +00:00
Maximilian Dorninger
bc3895ab40 Merge pull request #427 from maxdorninger/dependabot/npm_and_yarn/web/typeschema/class-validator-0.3.0
Bump @typeschema/class-validator from 0.2.0 to 0.3.0 in /web
2026-02-13 19:24:38 +01:00
Maximilian Dorninger
b7ed529f77 Merge pull request #428 from maxdorninger/dependabot/uv/starlette-0.52.1
Bump starlette from 0.50.0 to 0.52.1
2026-02-13 19:24:26 +01:00
Maximilian Dorninger
370df4efa0 Merge pull request #429 from maxdorninger/dependabot/npm_and_yarn/web/vite-7.3.1
Bump vite from 7.2.7 to 7.3.1 in /web
2026-02-13 19:24:12 +01:00
Maximilian Dorninger
a3e85d6338 Merge pull request #431 from maxdorninger/dependabot/npm_and_yarn/web/svelte-5.51.0
Bump svelte from 5.45.8 to 5.51.0 in /web
2026-02-13 19:24:01 +01:00
Maximilian Dorninger
a2816f2dfb Merge pull request #432 from maxdorninger/dependabot/npm_and_yarn/web/lucide-svelte-0.564.0
Bump lucide-svelte from 0.544.0 to 0.564.0 in /web
2026-02-13 19:23:48 +01:00
Maximilian Dorninger
0026b891f5 Merge pull request #430 from maxdorninger/dependabot/uv/cachetools-7.0.1
Bump cachetools from 6.2.4 to 7.0.1
2026-02-13 19:23:34 +01:00
dependabot[bot]
b312d880b7 Bump lucide-svelte from 0.544.0 to 0.564.0 in /web
Bumps [lucide-svelte](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-svelte) from 0.544.0 to 0.564.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.564.0/packages/lucide-svelte)

---
updated-dependencies:
- dependency-name: lucide-svelte
  dependency-version: 0.564.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:08:18 +00:00
dependabot[bot]
71e2a08535 Bump svelte from 5.45.8 to 5.51.0 in /web
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.45.8 to 5.51.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.51.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.51.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:08:10 +00:00
dependabot[bot]
f2bf1a2dae Bump cachetools from 6.2.4 to 7.0.1
Bumps [cachetools](https://github.com/tkem/cachetools) from 6.2.4 to 7.0.1.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v6.2.4...v7.0.1)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:08:06 +00:00
dependabot[bot]
6b70980c2a Bump vite from 7.2.7 to 7.3.1 in /web
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.2.7 to 7.3.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:08:01 +00:00
dependabot[bot]
e80a516c23 Bump starlette from 0.50.0 to 0.52.1
Bumps [starlette](https://github.com/Kludex/starlette) from 0.50.0 to 0.52.1.
- [Release notes](https://github.com/Kludex/starlette/releases)
- [Changelog](https://github.com/Kludex/starlette/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/starlette/compare/0.50.0...0.52.1)

---
updated-dependencies:
- dependency-name: starlette
  dependency-version: 0.52.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:07:55 +00:00
dependabot[bot]
280e136209 Bump @typeschema/class-validator from 0.2.0 to 0.3.0 in /web
Bumps [@typeschema/class-validator](https://github.com/decs/typeschema) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/decs/typeschema/releases)
- [Commits](https://github.com/decs/typeschema/compare/@typeschema/class-validator@0.2.0...@typeschema/class-validator@0.3.0)

---
updated-dependencies:
- dependency-name: "@typeschema/class-validator"
  dependency-version: 0.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 18:07:51 +00:00
maxid
5c62c9f5be Merge remote-tracking branch 'origin/master' 2026-02-13 19:06:44 +01:00
maxid
1e46cdc03b add metadata_relay to dependabot.yml 2026-02-13 19:06:36 +01:00
Maximilian Dorninger
18573fa7d9 Bump actions/setup-python from 5 to 6 (#412)
Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Upgrade to node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1164">actions/setup-python#1164</a></li>
</ul>
<p>Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">See
Release Notes</a></p>
<h3>Enhancements:</h3>
<ul>
<li>Add support for <code>pip-version</code> by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1129">actions/setup-python#1129</a></li>
<li>Enhance reading from .python-version by <a
href="https://github.com/krystof-k"><code>@​krystof-k</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/787">actions/setup-python#787</a></li>
<li>Add version parsing from Pipfile by <a
href="https://github.com/aradkdj"><code>@​aradkdj</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1067">actions/setup-python#1067</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Clarify pythonLocation behaviour for PyPy and GraalPy in environment
variables by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1183">actions/setup-python#1183</a></li>
<li>Change missing cache directory error to warning by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1182">actions/setup-python#1182</a></li>
<li>Add Architecture-Specific PATH Management for Python with --user
Flag on Windows by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1122">actions/setup-python#1122</a></li>
<li>Include python version in PyPy python-version output by <a
href="https://github.com/cdce8p"><code>@​cdce8p</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1110">actions/setup-python#1110</a></li>
<li>Update docs: clarification on pip authentication with setup-python
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1156">actions/setup-python#1156</a></li>
</ul>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade idna from 2.9 to 3.7 in /<strong>tests</strong>/data by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-python/pull/843">actions/setup-python#843</a></li>
<li>Upgrade form-data to fix critical vulnerabilities <a
href="https://redirect.github.com/actions/setup-python/issues/182">#182</a>
&amp; <a
href="https://redirect.github.com/actions/setup-python/issues/183">#183</a>
by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1163">actions/setup-python#1163</a></li>
<li>Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in
PackageIndex.download by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1165">actions/setup-python#1165</a></li>
<li>Upgrade actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-python/pull/1181">actions/setup-python#1181</a></li>
<li>Upgrade <code>@​actions/tool-cache</code> from 2.0.1 to 2.0.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-python/pull/1095">actions/setup-python#1095</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/krystof-k"><code>@​krystof-k</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/787">actions/setup-python#787</a></li>
<li><a href="https://github.com/cdce8p"><code>@​cdce8p</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1110">actions/setup-python#1110</a></li>
<li><a href="https://github.com/aradkdj"><code>@​aradkdj</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1067">actions/setup-python#1067</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v5...v6.0.0">https://github.com/actions/setup-python/compare/v5...v6.0.0</a></p>
<h2>v5.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Workflow updates related to Ubuntu 20.04 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1065">actions/setup-python#1065</a></li>
<li>Fix for Candidate Not Iterable Error by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1082">actions/setup-python#1082</a></li>
<li>Upgrade semver and <code>@​types/semver</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1091">actions/setup-python#1091</a></li>
<li>Upgrade prettier from 2.8.8 to 3.5.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1046">actions/setup-python#1046</a></li>
<li>Upgrade ts-jest from 29.1.2 to 29.3.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1081">actions/setup-python#1081</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v5...v5.6.0">https://github.com/actions/setup-python/compare/v5...v5.6.0</a></p>
<h2>v5.5.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support free threaded Python versions like '3.13t' by <a
href="https://github.com/colesbury"><code>@​colesbury</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/973">actions/setup-python#973</a></li>
<li>Enhance Workflows: Include ubuntu-arm runners, Add e2e Testing for
free threaded and Upgrade <code>@​action/cache</code> from 4.0.0 to
4.0.3 by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1056">actions/setup-python#1056</a></li>
<li>Add support for .tool-versions file in setup-python by <a
href="https://github.com/mahabaleshwars"><code>@​mahabaleshwars</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1043">actions/setup-python#1043</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Fix architecture for pypy on Linux ARM64 by <a
href="https://github.com/mayeut"><code>@​mayeut</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1011">actions/setup-python#1011</a>
This update maps arm64 to aarch64 for Linux ARM64 PyPy
installations.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a309ff8b42"><code>a309ff8</code></a>
Bump urllib3 from 2.6.0 to 2.6.3 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/1264">#1264</a>)</li>
<li><a
href="bfe8cc55a7"><code>bfe8cc5</code></a>
Upgrade <a href="https://github.com/actions"><code>@​actions</code></a>
dependencies to Node 24 compatible versions (<a
href="https://redirect.github.com/actions/setup-python/issues/1259">#1259</a>)</li>
<li><a
href="4f41a90a1f"><code>4f41a90</code></a>
Bump urllib3 from 2.5.0 to 2.6.0 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/1253">#1253</a>)</li>
<li><a
href="83679a892e"><code>83679a8</code></a>
Bump <code>@​types/node</code> from 24.1.0 to 24.9.1 and update macos-13
to macos-15-intel ...</li>
<li><a
href="bfc4944b43"><code>bfc4944</code></a>
Bump prettier from 3.5.3 to 3.6.2 (<a
href="https://redirect.github.com/actions/setup-python/issues/1234">#1234</a>)</li>
<li><a
href="97aeb3efb8"><code>97aeb3e</code></a>
Bump requests from 2.32.2 to 2.32.4 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/1130">#1130</a>)</li>
<li><a
href="443da59188"><code>443da59</code></a>
Bump actions/publish-action from 0.3.0 to 0.4.0 &amp; Documentation
update for pi...</li>
<li><a
href="cfd55ca824"><code>cfd55ca</code></a>
graalpy: add graalpy early-access and windows builds (<a
href="https://redirect.github.com/actions/setup-python/issues/880">#880</a>)</li>
<li><a
href="bba65e51ff"><code>bba65e5</code></a>
Bump typescript from 5.4.2 to 5.9.3 and update docs/advanced-usage.md
(<a
href="https://redirect.github.com/actions/setup-python/issues/1094">#1094</a>)</li>
<li><a
href="18566f86b3"><code>18566f8</code></a>
Improve wording and &quot;fix example&quot; (remove 3.13) on testing
against pre-releas...</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-python/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 19:04:05 +01:00
maxid
6debd7a42d update package-lock.json 2026-02-13 18:54:54 +01:00
39 changed files with 1887 additions and 1527 deletions

View File

@@ -23,3 +23,9 @@ updates:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 5 open-pull-requests-limit: 5
- package-ecosystem: "uv"
directory: "/metadata_relay"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

View File

@@ -1,7 +1,7 @@
<br /> <br />
<div align="center"> <div align="center">
<a href="https://maxdorninger.github.io/MediaManager/"> <a href="https://maxdorninger.github.io/MediaManager/">
<img src="https://raw.githubusercontent.com/maxdorninger/MediaManager/refs/heads/master/web/static/logo.svg" alt="Logo" width="260" height="260"> <img src="https://raw.githubusercontent.com/maxdorninger/MediaManager/refs/heads/master/docs/assets/logo-with-text.svg" alt="Logo" width="800">
</a> </a>
<h3 align="center">MediaManager</h3> <h3 align="center">MediaManager</h3>

View File

@@ -35,8 +35,8 @@ from media_manager.notification.models import Notification # noqa: E402
from media_manager.torrent.models import Torrent # noqa: E402 from media_manager.torrent.models import Torrent # noqa: E402
from media_manager.tv.models import ( # noqa: E402 from media_manager.tv.models import ( # noqa: E402
Episode, Episode,
EpisodeFile,
Season, Season,
SeasonFile,
SeasonRequest, SeasonRequest,
Show, Show,
) )
@@ -47,6 +47,7 @@ target_metadata = Base.metadata
# noinspection PyStatementEffect # noinspection PyStatementEffect
__all__ = [ __all__ = [
"Episode", "Episode",
"EpisodeFile",
"IndexerQueryResult", "IndexerQueryResult",
"Movie", "Movie",
"MovieFile", "MovieFile",
@@ -54,7 +55,6 @@ __all__ = [
"Notification", "Notification",
"OAuthAccount", "OAuthAccount",
"Season", "Season",
"SeasonFile",
"SeasonRequest", "SeasonRequest",
"Show", "Show",
"Torrent", "Torrent",

View File

@@ -0,0 +1,46 @@
"""create episode file table and add episode column to indexerqueryresult
Revision ID: 3a8fbd71e2c2
Revises: 9f3c1b2a4d8e
Create Date: 2026-01-08 13:43:00
"""
from typing import Sequence, Union
from alembic import op
from sqlalchemy.dialects import postgresql
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "3a8fbd71e2c2"
down_revision: Union[str, None] = "9f3c1b2a4d8e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
quality_enum = postgresql.ENUM("uhd", "fullhd", "hd", "sd", "unknown", name="quality",
create_type=False,
)
# Create episode file table
op.create_table(
"episode_file",
sa.Column("episode_id", sa.UUID(), nullable=False),
sa.Column("torrent_id", sa.UUID(), nullable=True),
sa.Column("file_path_suffix", sa.String(), nullable=False),
sa.Column("quality", quality_enum, nullable=False),
sa.ForeignKeyConstraint(["episode_id"], ["episode.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["torrent_id"], ["torrent.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("episode_id", "file_path_suffix"),
)
# Add episode column to indexerqueryresult
op.add_column(
"indexer_query_result", sa.Column("episode", postgresql.ARRAY(sa.Integer()), nullable=True),
)
def downgrade() -> None:
op.drop_table("episode_file")
op.drop_column("indexer_query_result", "episode")

View File

@@ -0,0 +1,31 @@
"""add overview column to episode table
Revision ID: 9f3c1b2a4d8e
Revises: 2c61f662ca9e
Create Date: 2025-12-29 21:45:00
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "9f3c1b2a4d8e"
down_revision: Union[str, None] = "2c61f662ca9e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add overview to episode table
op.add_column(
"episode",
sa.Column("overview", sa.Text(), nullable=True),
)
def downgrade() -> None:
op.drop_column("episode", "overview")

View File

@@ -0,0 +1,71 @@
"""migrate season files to episode files and drop the legacy table
Revision ID: a6f714d3c8b9
Revises: 16e78af9e5bf
Create Date: 2026-02-22 16:30:00
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "a6f714d3c8b9"
down_revision: Union[str, None] = "3a8fbd71e2c2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Copy season_file records into episode_file and remove the legacy table."""
op.execute(
"""
INSERT INTO episode_file (episode_id, torrent_id, file_path_suffix, quality)
SELECT episode.id, season_file.torrent_id, season_file.file_path_suffix, season_file.quality
FROM season_file
JOIN season ON season.id = season_file.season_id
JOIN episode ON episode.season_id = season.id
LEFT JOIN episode_file ON
episode_file.episode_id = episode.id
AND episode_file.file_path_suffix = season_file.file_path_suffix
WHERE episode_file.episode_id IS NULL
"""
)
op.drop_table("season_file")
def downgrade() -> None:
"""Recreate season_file, repopulate it from episode_file, and keep both tables."""
quality_enum = postgresql.ENUM(
"uhd", "fullhd", "hd", "sd", "unknown", name="quality", create_type=False
)
op.create_table(
"season_file",
sa.Column("season_id", sa.UUID(), nullable=False),
sa.Column("torrent_id", sa.UUID(), nullable=True),
sa.Column("file_path_suffix", sa.String(), nullable=False),
sa.Column("quality", quality_enum, nullable=False),
sa.ForeignKeyConstraint(["season_id"], ["season.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["torrent_id"], ["torrent.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("season_id", "file_path_suffix"),
)
op.execute(
"""
INSERT INTO season_file (season_id, torrent_id, file_path_suffix, quality)
SELECT DISTINCT ON (episode.season_id, episode_file.file_path_suffix)
episode.season_id,
episode_file.torrent_id,
episode_file.file_path_suffix,
episode_file.quality
FROM episode_file
JOIN episode ON episode.id = episode_file.episode_id
ORDER BY episode.season_id, episode_file.file_path_suffix, episode_file.torrent_id, episode_file.quality
"""
)

BIN
docs/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 110 KiB

22
docs/custom.css Normal file
View File

@@ -0,0 +1,22 @@
/*.md-header__button.md-logo {*/
/* margin-top: 0;*/
/* margin-bottom: 0;*/
/* padding-top: 0;*/
/* padding-bottom: 0;*/
/*}*/
/*.md-header__button.md-logo img,*/
/*.md-header__button.md-logo svg {*/
/* height: 70%;*/
/* width: 70%;*/
/*}*/
/* Increase logo size */
.md-header__button.md-logo svg, .md-header__button.md-logo img {
height: 2.5rem; /* Increase height (default is usually ~1.2rem to 1.5rem) */
width: auto;
}
/* Adjust header height if necessary to fit the larger logo */
.md-header {
height: 4rem; /* Match or exceed your new logo height */
}

View File

@@ -18,6 +18,7 @@ class IndexerQueryResult(Base):
flags = mapped_column(ARRAY(String)) flags = mapped_column(ARRAY(String))
quality: Mapped[Quality] quality: Mapped[Quality]
season = mapped_column(ARRAY(Integer)) season = mapped_column(ARRAY(Integer))
episode = mapped_column(ARRAY(Integer))
size = mapped_column(BigInteger) size = mapped_column(BigInteger)
usenet: Mapped[bool] usenet: Mapped[bool]
age: Mapped[int] age: Mapped[int]

View File

@@ -54,14 +54,55 @@ class IndexerQueryResult(BaseModel):
@computed_field @computed_field
@property @property
def season(self) -> list[int]: def season(self) -> list[int]:
pattern = r"\bS(\d+)\b" title = self.title.lower()
matches = re.findall(pattern, self.title, re.IGNORECASE)
if matches.__len__() == 2: # 1) S01E01 / S1E2
result = list(range(int(matches[0]), int(matches[1]) + 1)) m = re.search(r"s(\d{1,2})e\d{1,3}", title)
elif matches.__len__() == 1: if m:
result = [int(matches[0])] return [int(m.group(1))]
# 2) Range S01-S03 / S1-S3
m = re.search(r"s(\d{1,2})\s*(?:-|\u2013)\s*s?(\d{1,2})", title)
if m:
start, end = int(m.group(1)), int(m.group(2))
if start <= end:
return list(range(start, end + 1))
return []
# 3) Pack S01 / S1
m = re.search(r"\bs(\d{1,2})\b", title)
if m:
return [int(m.group(1))]
# 4) Season 01 / Season 1
m = re.search(r"\bseason\s*(\d{1,2})\b", title)
if m:
return [int(m.group(1))]
return []
@computed_field(return_type=list[int])
@property
def episode(self) -> list[int]:
title = self.title.lower()
result: list[int] = []
pattern = r"s\d{1,2}e(\d{1,3})(?:\s*-\s*(?:s?\d{1,2}e)?(\d{1,3}))?"
match = re.search(pattern, title)
if not match:
return result
start = int(match.group(1))
end = match.group(2)
if end:
end = int(end)
if end >= start:
result = list(range(start, end + 1))
else: else:
result = [] result = [start]
return result return result
def __gt__(self, other: "IndexerQueryResult") -> bool: def __gt__(self, other: "IndexerQueryResult") -> bool:

View File

@@ -16,5 +16,5 @@ class Torrent(Base):
hash: Mapped[str] hash: Mapped[str]
usenet: Mapped[bool] usenet: Mapped[bool]
season_files = relationship("SeasonFile", back_populates="torrent") episode_files = relationship("EpisodeFile", back_populates="torrent")
movie_files = relationship("MovieFile", back_populates="torrent") movie_files = relationship("MovieFile", back_populates="torrent")

View File

@@ -3,17 +3,13 @@ from sqlalchemy import delete, select
from media_manager.database import DbSessionDependency from media_manager.database import DbSessionDependency
from media_manager.exceptions import NotFoundError from media_manager.exceptions import NotFoundError
from media_manager.movies.models import Movie, MovieFile from media_manager.movies.models import Movie, MovieFile
from media_manager.movies.schemas import ( from media_manager.movies.schemas import Movie as MovieSchema
Movie as MovieSchema, from media_manager.movies.schemas import MovieFile as MovieFileSchema
)
from media_manager.movies.schemas import (
MovieFile as MovieFileSchema,
)
from media_manager.torrent.models import Torrent from media_manager.torrent.models import Torrent
from media_manager.torrent.schemas import Torrent as TorrentSchema from media_manager.torrent.schemas import Torrent as TorrentSchema
from media_manager.torrent.schemas import TorrentId from media_manager.torrent.schemas import TorrentId
from media_manager.tv.models import Season, SeasonFile, Show from media_manager.tv.models import Episode, EpisodeFile, Season, Show
from media_manager.tv.schemas import SeasonFile as SeasonFileSchema from media_manager.tv.schemas import EpisodeFile as EpisodeFileSchema
from media_manager.tv.schemas import Show as ShowSchema from media_manager.tv.schemas import Show as ShowSchema
@@ -21,19 +17,22 @@ class TorrentRepository:
def __init__(self, db: DbSessionDependency) -> None: def __init__(self, db: DbSessionDependency) -> None:
self.db = db self.db = db
def get_seasons_files_of_torrent( def get_episode_files_of_torrent(
self, torrent_id: TorrentId self, torrent_id: TorrentId
) -> list[SeasonFileSchema]: ) -> list[EpisodeFileSchema]:
stmt = select(SeasonFile).where(SeasonFile.torrent_id == torrent_id) stmt = select(EpisodeFile).where(EpisodeFile.torrent_id == torrent_id)
result = self.db.execute(stmt).scalars().all() result = self.db.execute(stmt).scalars().all()
return [SeasonFileSchema.model_validate(season_file) for season_file in result] return [
EpisodeFileSchema.model_validate(episode_file) for episode_file in result
]
def get_show_of_torrent(self, torrent_id: TorrentId) -> ShowSchema | None: def get_show_of_torrent(self, torrent_id: TorrentId) -> ShowSchema | None:
stmt = ( stmt = (
select(Show) select(Show)
.join(SeasonFile.season) .join(Show.seasons)
.join(Season.show) .join(Season.episodes)
.where(SeasonFile.torrent_id == torrent_id) .join(Episode.episode_files)
.where(EpisodeFile.torrent_id == torrent_id)
) )
result = self.db.execute(stmt).unique().scalar_one_or_none() result = self.db.execute(stmt).unique().scalar_one_or_none()
if result is None: if result is None:
@@ -69,10 +68,10 @@ class TorrentRepository:
) )
self.db.execute(movie_files_stmt) self.db.execute(movie_files_stmt)
season_files_stmt = delete(SeasonFile).where( episode_files_stmt = delete(EpisodeFile).where(
SeasonFile.torrent_id == torrent_id EpisodeFile.torrent_id == torrent_id
) )
self.db.execute(season_files_stmt) self.db.execute(episode_files_stmt)
self.db.delete(self.db.get(Torrent, torrent_id)) self.db.delete(self.db.get(Torrent, torrent_id))

View File

@@ -5,7 +5,7 @@ from media_manager.movies.schemas import Movie, MovieFile
from media_manager.torrent.manager import DownloadManager from media_manager.torrent.manager import DownloadManager
from media_manager.torrent.repository import TorrentRepository from media_manager.torrent.repository import TorrentRepository
from media_manager.torrent.schemas import Torrent, TorrentId from media_manager.torrent.schemas import Torrent, TorrentId
from media_manager.tv.schemas import SeasonFile, Show from media_manager.tv.schemas import EpisodeFile, Show
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -19,13 +19,13 @@ class TorrentService:
self.torrent_repository = torrent_repository self.torrent_repository = torrent_repository
self.download_manager = download_manager or DownloadManager() self.download_manager = download_manager or DownloadManager()
def get_season_files_of_torrent(self, torrent: Torrent) -> list[SeasonFile]: def get_episode_files_of_torrent(self, torrent: Torrent) -> list[EpisodeFile]:
""" """
Returns all season files of a torrent Returns all episode files of a torrent
:param torrent: the torrent to get the season files of :param torrent: the torrent to get the episode files of
:return: list of season files :return: list of episode files
""" """
return self.torrent_repository.get_seasons_files_of_torrent( return self.torrent_repository.get_episode_files_of_torrent(
torrent_id=torrent.id torrent_id=torrent.id
) )

View File

@@ -48,9 +48,6 @@ class Season(Base):
back_populates="season", cascade="all, delete" back_populates="season", cascade="all, delete"
) )
season_files = relationship(
"SeasonFile", back_populates="season", cascade="all, delete"
)
season_requests = relationship( season_requests = relationship(
"SeasonRequest", back_populates="season", cascade="all, delete" "SeasonRequest", back_populates="season", cascade="all, delete"
) )
@@ -66,15 +63,19 @@ class Episode(Base):
number: Mapped[int] number: Mapped[int]
external_id: Mapped[int] external_id: Mapped[int]
title: Mapped[str] title: Mapped[str]
overview: Mapped[str | None] = mapped_column(nullable=True)
season: Mapped["Season"] = relationship(back_populates="episodes") season: Mapped["Season"] = relationship(back_populates="episodes")
episode_files = relationship(
"EpisodeFile", back_populates="episode", cascade="all, delete"
)
class SeasonFile(Base): class EpisodeFile(Base):
__tablename__ = "season_file" __tablename__ = "episode_file"
__table_args__ = (PrimaryKeyConstraint("season_id", "file_path_suffix"),) __table_args__ = (PrimaryKeyConstraint("episode_id", "file_path_suffix"),)
season_id: Mapped[UUID] = mapped_column( episode_id: Mapped[UUID] = mapped_column(
ForeignKey(column="season.id", ondelete="CASCADE"), ForeignKey(column="episode.id", ondelete="CASCADE"),
) )
torrent_id: Mapped[UUID | None] = mapped_column( torrent_id: Mapped[UUID | None] = mapped_column(
ForeignKey(column="torrent.id", ondelete="SET NULL"), ForeignKey(column="torrent.id", ondelete="SET NULL"),
@@ -82,8 +83,8 @@ class SeasonFile(Base):
file_path_suffix: Mapped[str] file_path_suffix: Mapped[str]
quality: Mapped[Quality] quality: Mapped[Quality]
torrent = relationship("Torrent", back_populates="season_files", uselist=False) torrent = relationship("Torrent", back_populates="episode_files", uselist=False)
season = relationship("Season", back_populates="season_files", uselist=False) episode = relationship("Episode", back_populates="episode_files", uselist=False)
class SeasonRequest(Base): class SeasonRequest(Base):

View File

@@ -1,8 +1,5 @@
from sqlalchemy import delete, func, select from sqlalchemy import delete, func, select
from sqlalchemy.exc import ( from sqlalchemy.exc import IntegrityError, SQLAlchemyError
IntegrityError,
SQLAlchemyError,
)
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from media_manager.exceptions import ConflictError, NotFoundError from media_manager.exceptions import ConflictError, NotFoundError
@@ -10,32 +7,21 @@ from media_manager.torrent.models import Torrent
from media_manager.torrent.schemas import Torrent as TorrentSchema from media_manager.torrent.schemas import Torrent as TorrentSchema
from media_manager.torrent.schemas import TorrentId from media_manager.torrent.schemas import TorrentId
from media_manager.tv import log from media_manager.tv import log
from media_manager.tv.models import Episode, Season, SeasonFile, SeasonRequest, Show from media_manager.tv.models import Episode, EpisodeFile, Season, SeasonRequest, Show
from media_manager.tv.schemas import ( from media_manager.tv.schemas import Episode as EpisodeSchema
Episode as EpisodeSchema, from media_manager.tv.schemas import EpisodeFile as EpisodeFileSchema
)
from media_manager.tv.schemas import ( from media_manager.tv.schemas import (
EpisodeId, EpisodeId,
EpisodeNumber,
SeasonId, SeasonId,
SeasonNumber, SeasonNumber,
SeasonRequestId, SeasonRequestId,
ShowId, ShowId,
) )
from media_manager.tv.schemas import ( from media_manager.tv.schemas import RichSeasonRequest as RichSeasonRequestSchema
RichSeasonRequest as RichSeasonRequestSchema, from media_manager.tv.schemas import Season as SeasonSchema
) from media_manager.tv.schemas import SeasonRequest as SeasonRequestSchema
from media_manager.tv.schemas import ( from media_manager.tv.schemas import Show as ShowSchema
Season as SeasonSchema,
)
from media_manager.tv.schemas import (
SeasonFile as SeasonFileSchema,
)
from media_manager.tv.schemas import (
SeasonRequest as SeasonRequestSchema,
)
from media_manager.tv.schemas import (
Show as ShowSchema,
)
class TvRepository: class TvRepository:
@@ -120,9 +106,7 @@ class TvRepository:
def get_total_downloaded_episodes_count(self) -> int: def get_total_downloaded_episodes_count(self) -> int:
try: try:
stmt = ( stmt = select(func.count(Episode.id)).select_from(Episode).join(EpisodeFile)
select(func.count()).select_from(Episode).join(Season).join(SeasonFile)
)
return self.db.execute(stmt).scalar_one_or_none() return self.db.execute(stmt).scalar_one_or_none()
except SQLAlchemyError: except SQLAlchemyError:
log.exception("Database error while calculating downloaded episodes count") log.exception("Database error while calculating downloaded episodes count")
@@ -173,6 +157,7 @@ class TvRepository:
number=episode.number, number=episode.number,
external_id=episode.external_id, external_id=episode.external_id,
title=episode.title, title=episode.title,
overview=episode.overview,
) )
for episode in season.episodes for episode in season.episodes
], ],
@@ -234,6 +219,43 @@ class TvRepository:
log.exception(f"Database error while retrieving season {season_id}") log.exception(f"Database error while retrieving season {season_id}")
raise raise
def get_episode(self, episode_id: EpisodeId) -> EpisodeSchema:
"""
Retrieve an episode by its ID.
:param episode_id: The ID of the episode to get.
:return: An Episode object.
:raises NotFoundError: If the episode with the given ID is not found.
:raises SQLAlchemyError: If a database error occurs.
"""
try:
episode = self.db.get(Episode, episode_id)
if not episode:
msg = f"Episode with id {episode_id} not found."
raise NotFoundError(msg)
return EpisodeSchema.model_validate(episode)
except SQLAlchemyError as e:
log.error(f"Database error while retrieving episode {episode_id}: {e}")
raise
def get_season_by_episode(self, episode_id: EpisodeId) -> SeasonSchema:
try:
stmt = select(Season).join(Season.episodes).where(Episode.id == episode_id)
season = self.db.scalar(stmt)
if not season:
msg = f"Season not found for episode {episode_id}"
raise NotFoundError(msg)
return SeasonSchema.model_validate(season)
except SQLAlchemyError as e:
log.error(
f"Database error while retrieving season for episode {episode_id}: {e}"
)
raise
def add_season_request( def add_season_request(
self, season_request: SeasonRequestSchema self, season_request: SeasonRequestSchema
) -> SeasonRequestSchema: ) -> SeasonRequestSchema:
@@ -355,46 +377,46 @@ class TvRepository:
log.exception("Database error while retrieving season requests") log.exception("Database error while retrieving season requests")
raise raise
def add_season_file(self, season_file: SeasonFileSchema) -> SeasonFileSchema: def add_episode_file(self, episode_file: EpisodeFileSchema) -> EpisodeFileSchema:
""" """
Adds a season file record to the database. Adds an episode file record to the database.
:param season_file: The SeasonFile object to add. :param episode_file: The EpisodeFile object to add.
:return: The added SeasonFile object. :return: The added EpisodeFile object.
:raises IntegrityError: If the record violates constraints. :raises IntegrityError: If the record violates constraints.
:raises SQLAlchemyError: If a database error occurs. :raises SQLAlchemyError: If a database error occurs.
""" """
db_model = SeasonFile(**season_file.model_dump()) db_model = EpisodeFile(**episode_file.model_dump())
try: try:
self.db.add(db_model) self.db.add(db_model)
self.db.commit() self.db.commit()
self.db.refresh(db_model) self.db.refresh(db_model)
return SeasonFileSchema.model_validate(db_model) return EpisodeFileSchema.model_validate(db_model)
except IntegrityError: except IntegrityError as e:
self.db.rollback() self.db.rollback()
log.exception("Integrity error while adding season file") log.error(f"Integrity error while adding episode file: {e}")
raise raise
except SQLAlchemyError: except SQLAlchemyError as e:
self.db.rollback() self.db.rollback()
log.exception("Database error while adding season file") log.error(f"Database error while adding episode file: {e}")
raise raise
def remove_season_files_by_torrent_id(self, torrent_id: TorrentId) -> int: def remove_episode_files_by_torrent_id(self, torrent_id: TorrentId) -> int:
""" """
Removes season file records associated with a given torrent ID. Removes episode file records associated with a given torrent ID.
:param torrent_id: The ID of the torrent whose season files are to be removed. :param torrent_id: The ID of the torrent whose episode files are to be removed.
:return: The number of season files removed. :return: The number of episode files removed.
:raises SQLAlchemyError: If a database error occurs. :raises SQLAlchemyError: If a database error occurs.
""" """
try: try:
stmt = delete(SeasonFile).where(SeasonFile.torrent_id == torrent_id) stmt = delete(EpisodeFile).where(EpisodeFile.torrent_id == torrent_id)
result = self.db.execute(stmt) result = self.db.execute(stmt)
self.db.commit() self.db.commit()
except SQLAlchemyError: except SQLAlchemyError:
self.db.rollback() self.db.rollback()
log.exception( log.exception(
f"Database error removing season files for torrent_id {torrent_id}" f"Database error removing episode files for torrent_id {torrent_id}"
) )
raise raise
return result.rowcount return result.rowcount
@@ -420,23 +442,45 @@ class TvRepository:
log.exception(f"Database error setting library for show {show_id}") log.exception(f"Database error setting library for show {show_id}")
raise raise
def get_season_files_by_season_id( def get_episode_files_by_season_id(
self, season_id: SeasonId self, season_id: SeasonId
) -> list[SeasonFileSchema]: ) -> list[EpisodeFileSchema]:
""" """
Retrieve all season files for a given season ID. Retrieve all episode files for a given season ID.
:param season_id: The ID of the season. :param season_id: The ID of the season.
:return: A list of SeasonFile objects. :return: A list of EpisodeFile objects.
:raises SQLAlchemyError: If a database error occurs. :raises SQLAlchemyError: If a database error occurs.
""" """
try: try:
stmt = select(SeasonFile).where(SeasonFile.season_id == season_id) stmt = (
select(EpisodeFile).join(Episode).where(Episode.season_id == season_id)
)
results = self.db.execute(stmt).scalars().all() results = self.db.execute(stmt).scalars().all()
return [SeasonFileSchema.model_validate(sf) for sf in results] return [EpisodeFileSchema.model_validate(ef) for ef in results]
except SQLAlchemyError: except SQLAlchemyError:
log.exception( log.exception(
f"Database error retrieving season files for season_id {season_id}" f"Database error retrieving episode files for season_id {season_id}"
)
raise
def get_episode_files_by_episode_id(
self, episode_id: EpisodeId
) -> list[EpisodeFileSchema]:
"""
Retrieve all episode files for a given episode ID.
:param episode_id: The ID of the episode.
:return: A list of EpisodeFile objects.
:raises SQLAlchemyError: If a database error occurs.
"""
try:
stmt = select(EpisodeFile).where(EpisodeFile.episode_id == episode_id)
results = self.db.execute(stmt).scalars().all()
return [EpisodeFileSchema.model_validate(sf) for sf in results]
except SQLAlchemyError as e:
log.error(
f"Database error retrieving episode files for episode_id {episode_id}: {e}"
) )
raise raise
@@ -452,8 +496,9 @@ class TvRepository:
stmt = ( stmt = (
select(Torrent) select(Torrent)
.distinct() .distinct()
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id) .join(EpisodeFile, EpisodeFile.torrent_id == Torrent.id)
.join(Season, Season.id == SeasonFile.season_id) .join(Episode, Episode.id == EpisodeFile.episode_id)
.join(Season, Season.id == Episode.season_id)
.where(Season.show_id == show_id) .where(Season.show_id == show_id)
) )
results = self.db.execute(stmt).scalars().unique().all() results = self.db.execute(stmt).scalars().unique().all()
@@ -474,8 +519,9 @@ class TvRepository:
select(Show) select(Show)
.distinct() .distinct()
.join(Season, Show.id == Season.show_id) .join(Season, Show.id == Season.show_id)
.join(SeasonFile, Season.id == SeasonFile.season_id) .join(Episode, Season.id == Episode.season_id)
.join(Torrent, SeasonFile.torrent_id == Torrent.id) .join(EpisodeFile, Episode.id == EpisodeFile.episode_id)
.join(Torrent, EpisodeFile.torrent_id == Torrent.id)
.options(joinedload(Show.seasons).joinedload(Season.episodes)) .options(joinedload(Show.seasons).joinedload(Season.episodes))
.order_by(Show.name) .order_by(Show.name)
) )
@@ -497,8 +543,9 @@ class TvRepository:
stmt = ( stmt = (
select(Season.number) select(Season.number)
.distinct() .distinct()
.join(SeasonFile, Season.id == SeasonFile.season_id) .join(Episode, Episode.season_id == Season.id)
.where(SeasonFile.torrent_id == torrent_id) .join(EpisodeFile, EpisodeFile.episode_id == Episode.id)
.where(EpisodeFile.torrent_id == torrent_id)
) )
results = self.db.execute(stmt).scalars().unique().all() results = self.db.execute(stmt).scalars().unique().all()
return [SeasonNumber(x) for x in results] return [SeasonNumber(x) for x in results]
@@ -508,6 +555,32 @@ class TvRepository:
) )
raise raise
def get_episodes_by_torrent_id(self, torrent_id: TorrentId) -> list[EpisodeNumber]:
"""
Retrieve episode numbers associated with a given torrent ID.
:param torrent_id: The ID of the torrent.
:return: A list of EpisodeNumber objects.
:raises SQLAlchemyError: If a database error occurs.
"""
try:
stmt = (
select(Episode.number)
.join(EpisodeFile, EpisodeFile.episode_id == Episode.id)
.where(EpisodeFile.torrent_id == torrent_id)
.order_by(Episode.number)
)
episode_numbers = self.db.execute(stmt).scalars().all()
return [EpisodeNumber(n) for n in sorted(set(episode_numbers))]
except SQLAlchemyError as e:
log.error(
f"Database error retrieving episodes for torrent_id {torrent_id}: {e}"
)
raise
def get_season_request( def get_season_request(
self, season_request_id: SeasonRequestId self, season_request_id: SeasonRequestId
) -> SeasonRequestSchema: ) -> SeasonRequestSchema:
@@ -734,11 +807,15 @@ class TvRepository:
return SeasonSchema.model_validate(db_season) return SeasonSchema.model_validate(db_season)
def update_episode_attributes( def update_episode_attributes(
self, episode_id: EpisodeId, title: str | None = None self,
episode_id: EpisodeId,
title: str | None = None,
overview: str | None = None,
) -> EpisodeSchema: ) -> EpisodeSchema:
""" """
Update attributes of an existing episode. Update attributes of an existing episode.
:param overview: Tje new overview for the episode.
:param episode_id: The ID of the episode to update. :param episode_id: The ID of the episode to update.
:param title: The new title for the episode. :param title: The new title for the episode.
:param external_id: The new external ID for the episode. :param external_id: The new external ID for the episode.
@@ -755,6 +832,9 @@ class TvRepository:
if title is not None and db_episode.title != title: if title is not None and db_episode.title != title:
db_episode.title = title db_episode.title = title
updated = True updated = True
if overview is not None and db_episode.overview != overview:
db_episode.overview = overview
updated = True
if updated: if updated:
self.db.commit() self.db.commit()

View File

@@ -25,7 +25,7 @@ from media_manager.tv.dependencies import (
) )
from media_manager.tv.schemas import ( from media_manager.tv.schemas import (
CreateSeasonRequest, CreateSeasonRequest,
PublicSeasonFile, PublicEpisodeFile,
PublicShow, PublicShow,
RichSeasonRequest, RichSeasonRequest,
RichShowTorrent, RichShowTorrent,
@@ -402,13 +402,13 @@ def get_season(season: season_dep) -> Season:
"/seasons/{season_id}/files", "/seasons/{season_id}/files",
dependencies=[Depends(current_active_user)], dependencies=[Depends(current_active_user)],
) )
def get_season_files( def get_episode_files(
season: season_dep, tv_service: tv_service_dep season: season_dep, tv_service: tv_service_dep
) -> list[PublicSeasonFile]: ) -> list[PublicEpisodeFile]:
""" """
Get files associated with a specific season. Get episode files associated with a specific season.
""" """
return tv_service.get_public_season_files_by_season_id(season=season) return tv_service.get_public_episode_files_by_season_id(season=season)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -24,6 +24,7 @@ class Episode(BaseModel):
number: EpisodeNumber number: EpisodeNumber
external_id: int external_id: int
title: str title: str
overview: str | None = None
class Season(BaseModel): class Season(BaseModel):
@@ -98,16 +99,16 @@ class RichSeasonRequest(SeasonRequest):
season: Season season: Season
class SeasonFile(BaseModel): class EpisodeFile(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
season_id: SeasonId episode_id: EpisodeId
quality: Quality quality: Quality
torrent_id: TorrentId | None torrent_id: TorrentId | None
file_path_suffix: str file_path_suffix: str
class PublicSeasonFile(SeasonFile): class PublicEpisodeFile(EpisodeFile):
downloaded: bool = False downloaded: bool = False
@@ -123,6 +124,7 @@ class RichSeasonTorrent(BaseModel):
file_path_suffix: str file_path_suffix: str
seasons: list[SeasonNumber] seasons: list[SeasonNumber]
episodes: list[EpisodeNumber]
class RichShowTorrent(BaseModel): class RichShowTorrent(BaseModel):
@@ -135,6 +137,18 @@ class RichShowTorrent(BaseModel):
torrents: list[RichSeasonTorrent] torrents: list[RichSeasonTorrent]
class PublicEpisode(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: EpisodeId
number: EpisodeNumber
downloaded: bool = False
title: str
overview: str | None = None
external_id: int
class PublicSeason(BaseModel): class PublicSeason(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -147,7 +161,7 @@ class PublicSeason(BaseModel):
external_id: int external_id: int
episodes: list[Episode] episodes: list[PublicEpisode]
class PublicShow(BaseModel): class PublicShow(BaseModel):

View File

@@ -41,24 +41,24 @@ from media_manager.torrent.utils import (
from media_manager.tv import log from media_manager.tv import log
from media_manager.tv.repository import TvRepository from media_manager.tv.repository import TvRepository
from media_manager.tv.schemas import ( from media_manager.tv.schemas import (
Episode as EpisodeSchema, Episode,
) EpisodeFile,
from media_manager.tv.schemas import (
EpisodeId, EpisodeId,
EpisodeNumber,
PublicEpisodeFile,
PublicSeason, PublicSeason,
PublicSeasonFile,
PublicShow, PublicShow,
RichSeasonRequest, RichSeasonRequest,
RichSeasonTorrent, RichSeasonTorrent,
RichShowTorrent, RichShowTorrent,
Season, Season,
SeasonFile,
SeasonId, SeasonId,
SeasonRequest, SeasonRequest,
SeasonRequestId, SeasonRequestId,
Show, Show,
ShowId, ShowId,
) )
from media_manager.tv.schemas import Episode as EpisodeSchema
class TvService: class TvService:
@@ -173,6 +173,7 @@ class TvService:
for torrent in torrents: for torrent in torrents:
try: try:
self.torrent_service.cancel_download(torrent, delete_files=True) self.torrent_service.cancel_download(torrent, delete_files=True)
self.torrent_service.delete_torrent(torrent_id=torrent.id)
log.info(f"Deleted torrent: {torrent.hash}") log.info(f"Deleted torrent: {torrent.hash}")
except Exception: except Exception:
log.warning( log.warning(
@@ -181,24 +182,26 @@ class TvService:
self.tv_repository.delete_show(show_id=show.id) self.tv_repository.delete_show(show_id=show.id)
def get_public_season_files_by_season_id( def get_public_episode_files_by_season_id(
self, season: Season self, season: Season
) -> list[PublicSeasonFile]: ) -> list[PublicEpisodeFile]:
""" """
Get all public season files for a given season. Get all public episode files for a given season.
:param season: The season object. :param season: The season object.
:return: A list of public season files. :return: A list of public episode files.
""" """
season_files = self.tv_repository.get_season_files_by_season_id( episode_files = self.tv_repository.get_episode_files_by_season_id(
season_id=season.id season_id=season.id
) )
public_season_files = [PublicSeasonFile.model_validate(x) for x in season_files] public_episode_files = [
PublicEpisodeFile.model_validate(x) for x in episode_files
]
result = [] result = []
for season_file in public_season_files: for episode_file in public_episode_files:
if self.season_file_exists_on_file(season_file=season_file): if self.episode_file_exists_on_file(episode_file=episode_file):
season_file.downloaded = True episode_file.downloaded = True
result.append(season_file) result.append(episode_file)
return result return result
@overload @overload
@@ -334,11 +337,27 @@ class TvService:
:param show: The show object. :param show: The show object.
:return: A public show. :return: A public show.
""" """
seasons = [PublicSeason.model_validate(season) for season in show.seasons]
for season in seasons:
season.downloaded = self.is_season_downloaded(season_id=season.id)
public_show = PublicShow.model_validate(show) public_show = PublicShow.model_validate(show)
public_show.seasons = seasons public_seasons: list[PublicSeason] = []
for season in show.seasons:
public_season = PublicSeason.model_validate(season)
for episode in public_season.episodes:
episode.downloaded = self.is_episode_downloaded(
episode=episode,
season=season,
show=show,
)
# A season is considered downloaded if it has episodes and all of them are downloaded,
# matching the behavior of is_season_downloaded.
public_season.downloaded = bool(public_season.episodes) and all(
episode.downloaded for episode in public_season.episodes
)
public_seasons.append(public_season)
public_show.seasons = public_seasons
return public_show return public_show
def get_show_by_id(self, show_id: ShowId) -> Show: def get_show_by_id(self, show_id: ShowId) -> Show:
@@ -350,33 +369,85 @@ class TvService:
""" """
return self.tv_repository.get_show_by_id(show_id=show_id) return self.tv_repository.get_show_by_id(show_id=show_id)
def is_season_downloaded(self, season_id: SeasonId) -> bool: def is_season_downloaded(self, season: Season, show: Show) -> bool:
""" """
Check if a season is downloaded. Check if a season is downloaded.
:param season_id: The ID of the season. :param season: The season object.
:param show: The show object.
:return: True if the season is downloaded, False otherwise. :return: True if the season is downloaded, False otherwise.
""" """
season_files = self.tv_repository.get_season_files_by_season_id( episodes = season.episodes
season_id=season_id
if not episodes:
return False
for episode in episodes:
if not self.is_episode_downloaded(
episode=episode, season=season, show=show
):
return False
return True
def is_episode_downloaded(
self, episode: Episode, season: Season, show: Show
) -> bool:
"""
Check if an episode is downloaded and imported (file exists on disk).
An episode is considered downloaded if:
- There is at least one EpisodeFile in the database AND
- A matching episode file exists in the season directory on disk.
:param episode: The episode object.
:param season: The season object.
:param show: The show object.
:return: True if the episode is downloaded and imported, False otherwise.
"""
episode_files = self.tv_repository.get_episode_files_by_episode_id(
episode_id=episode.id
) )
for season_file in season_files:
if self.season_file_exists_on_file(season_file=season_file): if not episode_files:
return True return False
season_dir = self.get_root_season_directory(show, season.number)
if not season_dir.exists():
return False
episode_token = f"S{season.number:02d}E{episode.number:02d}"
video_extensions = {".mkv", ".mp4", ".avi", ".mov"}
try:
for file in season_dir.iterdir():
if (
file.is_file()
and episode_token.lower() in file.name.lower()
and file.suffix.lower() in video_extensions
):
return True
except OSError as e:
log.error(
f"Disk check failed for episode {episode.id} in {season_dir}: {e}"
)
return False return False
def season_file_exists_on_file(self, season_file: SeasonFile) -> bool: def episode_file_exists_on_file(self, episode_file: EpisodeFile) -> bool:
""" """
Check if a season file exists on the filesystem. Check if an episode file exists on the filesystem.
:param season_file: The season file to check. :param episode_file: The episode file to check.
:return: True if the file exists, False otherwise. :return: True if the file exists, False otherwise.
""" """
if season_file.torrent_id is None: if episode_file.torrent_id is None:
return True return True
try: try:
torrent_file = self.torrent_service.get_torrent_by_id( torrent_file = self.torrent_service.get_torrent_by_id(
torrent_id=season_file.torrent_id torrent_id=episode_file.torrent_id
) )
if torrent_file.imported: if torrent_file.imported:
@@ -409,6 +480,24 @@ class TvService:
""" """
return self.tv_repository.get_season(season_id=season_id) return self.tv_repository.get_season(season_id=season_id)
def get_episode(self, episode_id: EpisodeId) -> Episode:
"""
Get an episode by its ID.
:param episode_id: The ID of the episode.
:return: The episode.
"""
return self.tv_repository.get_episode(episode_id=episode_id)
def get_season_by_episode(self, episode_id: EpisodeId) -> Season:
"""
Get a season by the episode ID.
:param episode_id: The ID of the episode.
:return: The season.
"""
return self.tv_repository.get_season_by_episode(episode_id=episode_id)
def get_all_season_requests(self) -> list[RichSeasonRequest]: def get_all_season_requests(self) -> list[RichSeasonRequest]:
""" """
Get all season requests. Get all season requests.
@@ -430,10 +519,16 @@ class TvService:
seasons = self.tv_repository.get_seasons_by_torrent_id( seasons = self.tv_repository.get_seasons_by_torrent_id(
torrent_id=show_torrent.id torrent_id=show_torrent.id
) )
season_files = self.torrent_service.get_season_files_of_torrent( episodes = self.tv_repository.get_episodes_by_torrent_id(
torrent_id=show_torrent.id
)
episode_files = self.torrent_service.get_episode_files_of_torrent(
torrent=show_torrent torrent=show_torrent
) )
file_path_suffix = season_files[0].file_path_suffix if season_files else ""
file_path_suffix = (
episode_files[0].file_path_suffix if episode_files else ""
)
season_torrent = RichSeasonTorrent( season_torrent = RichSeasonTorrent(
torrent_id=show_torrent.id, torrent_id=show_torrent.id,
torrent_title=show_torrent.title, torrent_title=show_torrent.title,
@@ -441,10 +536,12 @@ class TvService:
quality=show_torrent.quality, quality=show_torrent.quality,
imported=show_torrent.imported, imported=show_torrent.imported,
seasons=seasons, seasons=seasons,
episodes=episodes if len(seasons) == 1 else [],
file_path_suffix=file_path_suffix, file_path_suffix=file_path_suffix,
usenet=show_torrent.usenet, usenet=show_torrent.usenet,
) )
rich_season_torrents.append(season_torrent) rich_season_torrents.append(season_torrent)
return RichShowTorrent( return RichShowTorrent(
show_id=show.id, show_id=show.id,
name=show.name, name=show.name,
@@ -487,24 +584,49 @@ class TvService:
season = self.tv_repository.get_season_by_number( season = self.tv_repository.get_season_by_number(
season_number=season_number, show_id=show_id season_number=season_number, show_id=show_id
) )
season_file = SeasonFile( episodes = {episode.number: episode.id for episode in season.episodes}
season_id=season.id,
quality=indexer_result.quality, if indexer_result.episode:
torrent_id=show_torrent.id, episode_ids = []
file_path_suffix=override_show_file_path_suffix, missing_episodes = []
) for ep_number in indexer_result.episode:
self.tv_repository.add_season_file(season_file=season_file) ep_id = episodes.get(EpisodeNumber(ep_number))
if ep_id is None:
missing_episodes.append(ep_number)
continue
episode_ids.append(ep_id)
if missing_episodes:
log.warning(
"Some episodes from indexer result were not found in season %s "
"for show %s and will be skipped: %s",
season.id,
show_id,
", ".join(str(ep) for ep in missing_episodes),
)
else:
episode_ids = [episode.id for episode in season.episodes]
for episode_id in episode_ids:
episode_file = EpisodeFile(
episode_id=episode_id,
quality=indexer_result.quality,
torrent_id=show_torrent.id,
file_path_suffix=override_show_file_path_suffix,
)
self.tv_repository.add_episode_file(episode_file=episode_file)
except IntegrityError: except IntegrityError:
log.error( log.error(
f"Season file for season {season.id} and quality {indexer_result.quality} already exists, skipping." f"Episode file for episode {episode_id} of season {season.id} and quality {indexer_result.quality} already exists, skipping."
) )
self.tv_repository.remove_episode_files_by_torrent_id(show_torrent.id)
self.torrent_service.cancel_download( self.torrent_service.cancel_download(
torrent=show_torrent, delete_files=True torrent=show_torrent, delete_files=True
) )
raise raise
else: else:
log.info( log.info(
f"Successfully added season files for torrent {show_torrent.title} and show ID {show_id}" f"Successfully added episode files for torrent {show_torrent.title} and show ID {show_id}"
) )
self.torrent_service.resume_download(torrent=show_torrent) self.torrent_service.resume_download(torrent=show_torrent)
@@ -561,7 +683,7 @@ class TvService:
available_torrents.sort() available_torrents.sort()
torrent = self.torrent_service.download(indexer_result=available_torrents[0]) torrent = self.torrent_service.download(indexer_result=available_torrents[0])
season_file = SeasonFile( season_file = SeasonFile( # noqa: F821
season_id=season.id, season_id=season.id,
quality=torrent.quality, quality=torrent.quality,
torrent_id=torrent.id, torrent_id=torrent.id,
@@ -653,12 +775,12 @@ class TvService:
video_files: list[Path], video_files: list[Path],
subtitle_files: list[Path], subtitle_files: list[Path],
file_path_suffix: str = "", file_path_suffix: str = "",
) -> tuple[bool, int]: ) -> tuple[bool, list[Episode]]:
season_path = self.get_root_season_directory( season_path = self.get_root_season_directory(
show=show, season_number=season.number show=show, season_number=season.number
) )
success = True success = True
imported_episodes_count = 0 imported_episodes = []
try: try:
season_path.mkdir(parents=True, exist_ok=True) season_path.mkdir(parents=True, exist_ok=True)
except Exception as e: except Exception as e:
@@ -677,7 +799,7 @@ class TvService:
file_path_suffix=file_path_suffix, file_path_suffix=file_path_suffix,
) )
if imported: if imported:
imported_episodes_count += 1 imported_episodes.append(episode)
except Exception: except Exception:
# Send notification about missing episode file # Send notification about missing episode file
@@ -690,11 +812,72 @@ class TvService:
log.warning( log.warning(
f"S{season.number}E{episode.number} not found when trying to import episode for show {show.name}." f"S{season.number}E{episode.number} not found when trying to import episode for show {show.name}."
) )
return success, imported_episodes_count return success, imported_episodes
def import_torrent_files(self, torrent: Torrent, show: Show) -> None: def import_episode_files(
self,
show: Show,
season: Season,
episode: Episode,
video_files: list[Path],
subtitle_files: list[Path],
file_path_suffix: str = "",
) -> bool:
episode_file_name = f"{remove_special_characters(show.name)} S{season.number:02d}E{episode.number:02d}"
if file_path_suffix != "":
episode_file_name += f" - {file_path_suffix}"
pattern = (
r".*[. ]S0?" + str(season.number) + r"E0?" + str(episode.number) + r"[. ].*"
)
subtitle_pattern = pattern + r"[. ]([A-Za-z]{2})[. ]srt"
target_file_name = (
self.get_root_season_directory(show=show, season_number=season.number)
/ episode_file_name
)
# import subtitle
for subtitle_file in subtitle_files:
regex_result = re.search(
subtitle_pattern, subtitle_file.name, re.IGNORECASE
)
if regex_result:
language_code = regex_result.group(1)
target_subtitle_file = target_file_name.with_suffix(
f".{language_code}.srt"
)
import_file(target_file=target_subtitle_file, source_file=subtitle_file)
else:
log.debug(
f"Didn't find any pattern {subtitle_pattern} in subtitle file: {subtitle_file.name}"
)
found_video = False
# import episode videos
for file in video_files:
if re.search(pattern, file.name, re.IGNORECASE):
target_video_file = target_file_name.with_suffix(file.suffix)
import_file(target_file=target_video_file, source_file=file)
found_video = True
break
if not found_video:
# Send notification about missing episode file
if self.notification_service:
self.notification_service.send_notification_to_all_providers(
title="Missing Episode File",
message=f"No video file found for S{season.number:02d}E{episode.number:02d} for show {show.name}. Manual intervention may be required.",
)
log.warning(
f"File for S{season.number}E{episode.number} not found when trying to import episode for show {show.name}."
)
return False
return True
def import_episode_files_from_torrent(self, torrent: Torrent, show: Show) -> None:
""" """
Organizes files from a torrent into the TV directory structure, mapping them to seasons and episodes. Organizes episodes files from a torrent into the TV directory structure, mapping them to seasons and episodes.
:param torrent: The Torrent object :param torrent: The Torrent object
:param show: The Show object :param show: The Show object
""" """
@@ -707,33 +890,68 @@ class TvService:
f"Importing these {len(video_files)} files:\n" + pprint.pformat(video_files) f"Importing these {len(video_files)} files:\n" + pprint.pformat(video_files)
) )
season_files = self.torrent_service.get_season_files_of_torrent(torrent=torrent) episode_files = self.torrent_service.get_episode_files_of_torrent(
torrent=torrent
)
if not episode_files:
log.warning(
f"No episode files associated with torrent {torrent.title}, skipping import."
)
return
log.info( log.info(
f"Found {len(season_files)} season files associated with torrent {torrent.title}" f"Found {len(episode_files)} episode files associated with torrent {torrent.title}"
) )
for season_file in season_files: imported_episodes_by_season: dict[int, list[int]] = {}
season = self.get_season(season_id=season_file.season_id)
season_import_success, _imported_episodes_count = self.import_season( for episode_file in episode_files:
season = self.get_season_by_episode(episode_id=episode_file.episode_id)
episode = self.get_episode(episode_file.episode_id)
season_path = self.get_root_season_directory(
show=show, season_number=season.number
)
if not season_path.exists():
try:
season_path.mkdir(parents=True)
except Exception as e:
log.warning(f"Could not create path {season_path}: {e}")
msg = f"Could not create path {season_path}"
raise Exception(msg) from e # noqa: TRY002
episoded_import_success = self.import_episode_files(
show=show, show=show,
season=season, season=season,
episode=episode,
video_files=video_files, video_files=video_files,
subtitle_files=subtitle_files, subtitle_files=subtitle_files,
file_path_suffix=season_file.file_path_suffix, file_path_suffix=episode_file.file_path_suffix,
) )
success.append(season_import_success) success.append(episoded_import_success)
if season_import_success:
if episoded_import_success:
imported_episodes_by_season.setdefault(season.number, []).append(
episode.number
)
log.info( log.info(
f"Season {season.number} successfully imported from torrent {torrent.title}" f"Episode {episode.number} from Season {season.number} successfully imported from torrent {torrent.title}"
) )
else: else:
log.warning( log.warning(
f"Season {season.number} failed to import from torrent {torrent.title}" f"Episode {episode.number} from Season {season.number} failed to import from torrent {torrent.title}"
) )
log.info( success_messages: list[str] = []
f"Finished importing files for torrent {torrent.title} {'without' if all(success) else 'with'} errors"
) for season_number, episodes in imported_episodes_by_season.items():
episode_list = ",".join(str(e) for e in sorted(episodes))
success_messages.append(
f"Episode(s): {episode_list} from Season {season_number}"
)
episodes_summary = "; ".join(success_messages)
if all(success): if all(success):
torrent.imported = True torrent.imported = True
@@ -743,7 +961,11 @@ class TvService:
if self.notification_service: if self.notification_service:
self.notification_service.send_notification_to_all_providers( self.notification_service.send_notification_to_all_providers(
title="TV Show imported successfully", title="TV Show imported successfully",
message=f"Successfully imported {show.name} ({show.year}) from torrent {torrent.title}.", message=(
f"Successfully imported {episodes_summary} "
f"of {show.name} ({show.year}) "
f"from torrent {torrent.title}."
),
) )
else: else:
if self.notification_service: if self.notification_service:
@@ -752,6 +974,10 @@ class TvService:
message=f"Importing {show.name} ({show.year}) from torrent {torrent.title} completed with errors. Please check the logs for details.", message=f"Importing {show.name} ({show.year}) from torrent {torrent.title} completed with errors. Please check the logs for details.",
) )
log.info(
f"Finished importing files for torrent {torrent.title} {'without' if all(success) else 'with'} errors"
)
def update_show_metadata( def update_show_metadata(
self, db_show: Show, metadata_provider: AbstractMetadataProvider self, db_show: Show, metadata_provider: AbstractMetadataProvider
) -> Show | None: ) -> Show | None:
@@ -823,6 +1049,7 @@ class TvService:
self.tv_repository.update_episode_attributes( self.tv_repository.update_episode_attributes(
episode_id=existing_episode.id, episode_id=existing_episode.id,
title=fresh_episode_data.title, title=fresh_episode_data.title,
overview=fresh_episode_data.overview,
) )
else: else:
# Add new episode # Add new episode
@@ -834,6 +1061,7 @@ class TvService:
number=fresh_episode_data.number, number=fresh_episode_data.number,
external_id=fresh_episode_data.external_id, external_id=fresh_episode_data.external_id,
title=fresh_episode_data.title, title=fresh_episode_data.title,
overview=fresh_episode_data.overview,
) )
self.tv_repository.add_episode_to_season( self.tv_repository.add_episode_to_season(
season_id=existing_season.id, episode_data=episode_schema season_id=existing_season.id, episode_data=episode_schema
@@ -849,6 +1077,7 @@ class TvService:
number=ep_data.number, number=ep_data.number,
external_id=ep_data.external_id, external_id=ep_data.external_id,
title=ep_data.title, title=ep_data.title,
overview=ep_data.overview,
) )
for ep_data in fresh_season_data.episodes for ep_data in fresh_season_data.episodes
] ]
@@ -911,21 +1140,22 @@ class TvService:
directory=new_source_path directory=new_source_path
) )
for season in tv_show.seasons: for season in tv_show.seasons:
success, imported_episode_count = self.import_season( _success, imported_episodes = self.import_season(
show=tv_show, show=tv_show,
season=season, season=season,
video_files=video_files, video_files=video_files,
subtitle_files=subtitle_files, subtitle_files=subtitle_files,
file_path_suffix="IMPORTED", file_path_suffix="IMPORTED",
) )
season_file = SeasonFile( for episode in imported_episodes:
season_id=season.id, episode_file = EpisodeFile(
quality=Quality.unknown, episode_id=episode.id,
file_path_suffix="IMPORTED", quality=Quality.unknown,
torrent_id=None, file_path_suffix="IMPORTED",
) torrent_id=None,
if success or imported_episode_count > (len(season.episodes) / 2): )
self.tv_repository.add_season_file(season_file=season_file)
self.tv_repository.add_episode_file(episode_file=episode_file)
def get_importable_tv_shows( def get_importable_tv_shows(
self, metadata_provider: AbstractMetadataProvider self, metadata_provider: AbstractMetadataProvider
@@ -1029,9 +1259,12 @@ def import_all_show_torrents() -> None:
f"torrent {t.title} is not a tv torrent, skipping import." f"torrent {t.title} is not a tv torrent, skipping import."
) )
continue continue
tv_service.import_torrent_files(torrent=t, show=show) tv_service.import_episode_files_from_torrent(torrent=t, show=show)
except RuntimeError: except RuntimeError as e:
log.exception(f"Error importing torrent {t.title} for show {show.name}") log.error(
f"Error importing torrent {t.title} for show {show.name}: {e}",
exc_info=True,
)
log.info("Finished importing all torrents") log.info("Finished importing all torrents")
db.commit() db.commit()

18
metadata_relay/uv.lock generated
View File

@@ -547,11 +547,11 @@ wheels = [
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.21" version = "0.0.22"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
] ]
[[package]] [[package]]
@@ -791,24 +791,24 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.2" version = "2.6.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
] ]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.40.0" version = "0.41.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]

View File

@@ -2,7 +2,7 @@ site_name: "MediaManager Documentation"
theme: theme:
name: "material" name: "material"
logo: "assets/logo.svg" logo: "assets/logo.svg"
favicon: "assets/logo.svg" favicon: "assets/favicon.ico"
features: features:
- navigation.sections - navigation.sections
- navigation.expand - navigation.expand
@@ -68,3 +68,5 @@ nav:
extra: extra:
version: version:
provider: mike provider: mike
extra_css:
- custom.css

250
uv.lock generated
View File

@@ -8,16 +8,16 @@ resolution-markers = [
[[package]] [[package]]
name = "alembic" name = "alembic"
version = "1.17.2" version = "1.18.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "mako" }, { name = "mako" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" },
] ]
[[package]] [[package]]
@@ -201,11 +201,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/ea/fe/67e991cb1df3e9c94
[[package]] [[package]]
name = "cachetools" name = "cachetools"
version = "6.2.4" version = "7.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
] ]
[[package]] [[package]]
@@ -326,58 +326,55 @@ wheels = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.3" version = "46.0.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
] ]
[[package]] [[package]]
@@ -404,17 +401,18 @@ wheels = [
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.128.0" version = "0.129.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-doc" }, { name = "annotated-doc" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "starlette" }, { name = "starlette" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } sdist = { url = "https://files.pythonhosted.org/packages/48/47/75f6bea02e797abff1bca968d5997793898032d9923c1935ae2efdece642/fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af", size = 375450, upload-time = "2026-02-12T13:54:52.541Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, { url = "https://files.pythonhosted.org/packages/9e/dd/d0ee25348ac58245ee9f90b6f3cbb666bf01f69be7e0911f9851bddbda16/fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec", size = 102950, upload-time = "2026-02-12T13:54:54.528Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -491,7 +489,7 @@ all = [
[[package]] [[package]]
name = "fastapi-users" name = "fastapi-users"
version = "15.0.3" version = "15.0.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "email-validator" }, { name = "email-validator" },
@@ -501,9 +499,9 @@ dependencies = [
{ name = "pyjwt", extra = ["crypto"] }, { name = "pyjwt", extra = ["crypto"] },
{ name = "python-multipart" }, { name = "python-multipart" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/4f/35/7272d1c6c81828a1f1a4dca2d1731e8a428ae52c36404d78f7602d5fa044/fastapi_users-15.0.3.tar.gz", hash = "sha256:94b24f8889b51ca3d8da92a88bced2bca2764cb1dd21c7d6d838890ff57b6472", size = 121336, upload-time = "2025-12-19T09:41:09.488Z" } sdist = { url = "https://files.pythonhosted.org/packages/4b/52/fadeae2c8435fb457a9cd91e402639fa5c9a25b16e6d204e043bf00cd875/fastapi_users-15.0.4.tar.gz", hash = "sha256:62657a4323de929cd98697b0fbdea77773ef271a6b57ef359080b9f773ebe144", size = 121394, upload-time = "2026-02-05T09:36:41.194Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/a0/81b33000d59eb265b88074c6a15c1fda6f9120581878c368a94e87638d96/fastapi_users-15.0.3-py3-none-any.whl", hash = "sha256:cea3da00ba1bfdd04ce61dcb4515a0914f19d9609d3ba68cf54367c876f380c3", size = 39031, upload-time = "2025-12-19T09:41:10.34Z" }, { url = "https://files.pythonhosted.org/packages/71/48/5fb2a18227ccbd5138515f21fc4fa8abcd9982238de43511d7f941e708db/fastapi_users-15.0.4-py3-none-any.whl", hash = "sha256:30940894825e1dd7b86f6013e4bc75eccc25ae8ce5261d1b180f6411bb28aff4", size = 39037, upload-time = "2026-02-05T09:36:42.195Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -983,60 +981,60 @@ wheels = [
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.1.0" version = "12.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
] ]
[[package]] [[package]]
@@ -1250,11 +1248,11 @@ wheels = [
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.10.1" version = "2.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -1298,11 +1296,11 @@ wheels = [
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.21" version = "0.0.22"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
] ]
[[package]] [[package]]
@@ -1626,14 +1624,14 @@ asyncio = [
[[package]] [[package]]
name = "starlette" name = "starlette"
version = "0.50.0" version = "0.52.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
] ]
[[package]] [[package]]
@@ -1800,24 +1798,24 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.2" version = "2.6.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
] ]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.40.0" version = "0.41.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]

862
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,18 +21,18 @@
"@fontsource/fira-mono": "^5.0.0", "@fontsource/fira-mono": "^5.0.0",
"@lucide/svelte": "^0.482.0", "@lucide/svelte": "^0.482.0",
"@neoconfetti/svelte": "^2.0.0", "@neoconfetti/svelte": "^2.0.0",
"@sinclair/typebox": "^0.34.38", "@sinclair/typebox": "^0.34.48",
"@sveltejs/adapter-auto": "^6.0.2", "@sveltejs/adapter-auto": "^6.0.2",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.1", "@sveltejs/enhanced-img": "^0.6.1",
"@sveltejs/kit": "^2.27.3", "@sveltejs/kit": "^2.51.0",
"@sveltejs/vite-plugin-svelte": "^6.1.1", "@sveltejs/vite-plugin-svelte": "^6.1.1",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tanstack/table-core": "^8.21.3", "@tanstack/table-core": "^8.21.3",
"@typeschema/class-validator": "^0.2.0", "@typeschema/class-validator": "^0.3.0",
"@vinejs/vine": "^1.8.0", "@vinejs/vine": "^1.8.0",
"arktype": "^2.1.20", "arktype": "^2.1.20",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
@@ -50,13 +50,13 @@
"openapi-typescript": "^7.9.1", "openapi-typescript": "^7.9.1",
"paneforge": "^1.0.0-next.6", "paneforge": "^1.0.0-next.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.1", "prettier-plugin-tailwindcss": "^0.7.1",
"superstruct": "^2.0.2", "superstruct": "^2.0.2",
"svelte": "^5.38.0", "svelte": "^5.53.0",
"svelte-check": "^4.3.1", "svelte-check": "^4.3.1",
"svelte-sonner": "^1.0.7", "svelte-sonner": "^1.0.7",
"sveltekit-superforms": "^2.27.1", "sveltekit-superforms": "^2.29.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
@@ -65,13 +65,13 @@
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.39.1",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"vaul-svelte": "^1.0.0-next.7", "vaul-svelte": "^1.0.0-next.7",
"vite": "^7.1.1", "vite": "^7.3.1",
"yup": "^1.7.0", "yup": "^1.7.0",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"dependencies": { "dependencies": {
"animejs": "^4.2.2", "animejs": "^4.2.2",
"lucide-svelte": "^0.544.0", "lucide-svelte": "^0.574.0",
"openapi-fetch": "^0.14.0", "openapi-fetch": "^0.14.0",
"uuid": "^11.1.0" "uuid": "^11.1.0"
} }

View File

@@ -626,10 +626,10 @@ export interface paths {
cookie?: never; cookie?: never;
}; };
/** /**
* Get Season Files * Get Episode Files
* @description Get files associated with a specific season. * @description Get episode files associated with a specific season.
*/ */
get: operations['get_season_files_api_v1_tv_seasons__season_id__files_get']; get: operations['get_episode_files_api_v1_tv_seasons__season_id__files_get'];
put?: never; put?: never;
post?: never; post?: never;
delete?: never; delete?: never;
@@ -1316,6 +1316,8 @@ export interface components {
external_id: number; external_id: number;
/** Title */ /** Title */
title: string; title: string;
/** Overview */
overview?: string | null;
}; };
/** ErrorModel */ /** ErrorModel */
ErrorModel: { ErrorModel: {
@@ -1360,6 +1362,8 @@ export interface components {
readonly quality: components['schemas']['Quality']; readonly quality: components['schemas']['Quality'];
/** Season */ /** Season */
readonly season: number[]; readonly season: number[];
/** Episode */
readonly episode: number[];
}; };
/** LibraryItem */ /** LibraryItem */
LibraryItem: { LibraryItem: {
@@ -1504,6 +1508,45 @@ export interface components {
/** Authorization Url */ /** Authorization Url */
authorization_url: string; authorization_url: string;
}; };
/** PublicEpisode */
PublicEpisode: {
/**
* Id
* Format: uuid
*/
id: string;
/** Number */
number: number;
/**
* Downloaded
* @default false
*/
downloaded: boolean;
/** Title */
title: string;
/** Overview */
overview?: string | null;
/** External Id */
external_id: number;
};
/** PublicEpisodeFile */
PublicEpisodeFile: {
/**
* Episode Id
* Format: uuid
*/
episode_id: string;
quality: components['schemas']['Quality'];
/** Torrent Id */
torrent_id: string | null;
/** File Path Suffix */
file_path_suffix: string;
/**
* Downloaded
* @default false
*/
downloaded: boolean;
};
/** PublicMovie */ /** PublicMovie */
PublicMovie: { PublicMovie: {
/** /**
@@ -1580,25 +1623,7 @@ export interface components {
/** External Id */ /** External Id */
external_id: number; external_id: number;
/** Episodes */ /** Episodes */
episodes: components['schemas']['Episode'][]; episodes: components['schemas']['PublicEpisode'][];
};
/** PublicSeasonFile */
PublicSeasonFile: {
/**
* Season Id
* Format: uuid
*/
season_id: string;
quality: components['schemas']['Quality'];
/** Torrent Id */
torrent_id: string | null;
/** File Path Suffix */
file_path_suffix: string;
/**
* Downloaded
* @default false
*/
downloaded: boolean;
}; };
/** PublicShow */ /** PublicShow */
PublicShow: { PublicShow: {
@@ -1719,6 +1744,8 @@ export interface components {
file_path_suffix: string; file_path_suffix: string;
/** Seasons */ /** Seasons */
seasons: number[]; seasons: number[];
/** Episodes */
episodes: number[];
}; };
/** RichShowTorrent */ /** RichShowTorrent */
RichShowTorrent: { RichShowTorrent: {
@@ -3232,7 +3259,7 @@ export interface operations {
}; };
}; };
}; };
get_season_files_api_v1_tv_seasons__season_id__files_get: { get_episode_files_api_v1_tv_seasons__season_id__files_get: {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
@@ -3250,7 +3277,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
'application/json': components['schemas']['PublicSeasonFile'][]; 'application/json': components['schemas']['PublicEpisodeFile'][];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */

View File

@@ -0,0 +1,164 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { toast } from 'svelte-sonner';
import { formatSecondsToOptimalUnit } from '$lib/utils.ts';
import * as Table from '$lib/components/ui/table';
import { Badge } from '$lib/components/ui/badge';
import client from '$lib/api';
import type { components } from '$lib/api/api';
import SelectFilePathSuffixDialog from '$lib/components/download-dialogs/select-file-path-suffix-dialog.svelte';
import { invalidateAll } from '$app/navigation';
import TorrentTable from '$lib/components/download-dialogs/torrent-table.svelte';
import DownloadDialogWrapper from '$lib/components/download-dialogs/download-dialog-wrapper.svelte';
import { getFullyQualifiedMediaName } from '$lib/utils';
let { show }: { show: components['schemas']['Show'] } = $props();
let dialogueState = $state(false);
let torrentsError: string | null = $state(null);
let queryOverride: string = $state('');
let filePathSuffix: string = $state('');
let torrentsPromise: any = $state();
let torrentsData: any[] | null = $state(null);
let isLoading: boolean = $state(false);
const tableColumnHeadings = [
{ name: 'Size', id: 'size' },
{ name: 'Usenet', id: 'usenet' },
{ name: 'Seeders', id: 'seeders' },
{ name: 'Age', id: 'age' },
{ name: 'Score', id: 'score' },
{ name: 'Indexer', id: 'indexer' },
{ name: 'Indexer Flags', id: 'flags' },
{ name: 'Seasons', id: 'season' }
];
async function downloadTorrent(result_id: string) {
torrentsError = null;
const { response } = await client.POST('/api/v1/tv/torrents', {
params: {
query: {
show_id: show.id!,
public_indexer_result_id: result_id,
override_file_path_suffix: filePathSuffix === '' ? undefined : filePathSuffix
}
}
});
if (response.status === 409) {
const errorMessage = `There already is a File using the Filepath Suffix '${filePathSuffix}'. Try again with a different Filepath Suffix.`;
console.warn(errorMessage);
torrentsError = errorMessage;
if (dialogueState) toast.info(errorMessage);
} else if (!response.ok) {
const errorMessage = `Failed to download torrent for show ${show.id}: ${response.statusText}`;
console.error(errorMessage);
torrentsError = errorMessage;
toast.error(errorMessage);
} else {
toast.success('Torrent download started successfully!');
}
await invalidateAll();
}
async function search() {
if (!queryOverride || queryOverride.trim() === '') {
toast.error('Please enter a custom query.');
return;
}
isLoading = true;
torrentsError = null;
torrentsData = null;
torrentsPromise = client
.GET('/api/v1/tv/torrents', {
params: {
query: {
show_id: show.id!,
search_query_override: queryOverride
}
}
})
.then((data) => data?.data)
.finally(() => (isLoading = false));
toast.info('Searching for torrents...');
torrentsData = await torrentsPromise;
if (!torrentsData || torrentsData.length === 0) {
toast.info('No torrents found.');
} else {
toast.success(`Found ${torrentsData.length} torrents.`);
}
}
</script>
<DownloadDialogWrapper
bind:open={dialogueState}
triggerText="Custom Download"
title="Custom Torrent Download"
description="Search and download torrents using a fully custom query string."
>
<div class="grid w-full items-center gap-1.5">
<Label for="query-override">Enter a custom query</Label>
<div class="flex w-full max-w-sm items-center space-x-2">
<Input
bind:value={queryOverride}
id="query-override"
type="text"
placeholder={`e.g. ${getFullyQualifiedMediaName(show)} S01 1080p BluRay`}
/>
<Button disabled={isLoading} class="w-fit" onclick={search}>Search</Button>
</div>
<p class="text-sm text-muted-foreground">
The custom query completely overrides the default search logic. Make sure the torrent title
matches the episodes you want imported.
</p>
</div>
{#if torrentsError}
<div class="my-2 w-full text-center text-red-500">
An error occurred: {torrentsError}
</div>
{/if}
<TorrentTable {torrentsPromise} columns={tableColumnHeadings}>
{#snippet rowSnippet(torrent)}
<Table.Cell class="font-medium">{torrent.title}</Table.Cell>
<Table.Cell>{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB</Table.Cell>
<Table.Cell>{torrent.usenet}</Table.Cell>
<Table.Cell>{torrent.usenet ? 'N/A' : torrent.seeders}</Table.Cell>
<Table.Cell>
{torrent.age ? formatSecondsToOptimalUnit(torrent.age) : torrent.usenet ? 'N/A' : ''}
</Table.Cell>
<Table.Cell>{torrent.score}</Table.Cell>
<Table.Cell>{torrent.indexer ?? 'unknown'}</Table.Cell>
<Table.Cell>
{#if torrent.flags}
{#each torrent.flags as flag (flag)}
<Badge variant="outline">{flag}</Badge>
{/each}
{/if}
</Table.Cell>
<Table.Cell>
{torrent.season ?? '-'}
</Table.Cell>
<Table.Cell class="text-right">
<SelectFilePathSuffixDialog
bind:filePathSuffix
media={show}
callback={() => downloadTorrent(torrent.id)}
/>
</Table.Cell>
{/snippet}
</TorrentTable>
</DownloadDialogWrapper>

View File

@@ -0,0 +1,188 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { toast } from 'svelte-sonner';
import { formatSecondsToOptimalUnit } from '$lib/utils';
import * as Table from '$lib/components/ui/table';
import { Badge } from '$lib/components/ui/badge';
import client from '$lib/api';
import type { components } from '$lib/api/api';
import SelectFilePathSuffixDialog from '$lib/components/download-dialogs/select-file-path-suffix-dialog.svelte';
import { invalidateAll } from '$app/navigation';
import TorrentTable from '$lib/components/download-dialogs/torrent-table.svelte';
import DownloadDialogWrapper from '$lib/components/download-dialogs/download-dialog-wrapper.svelte';
let {
show,
selectedEpisodeNumbers,
triggerText = 'Download Episodes'
}: {
show: components['schemas']['Show'];
selectedEpisodeNumbers: { seasonNumber: number; episodeNumber: number }[];
triggerText?: string;
} = $props();
let dialogueState = $state(false);
let torrentsPromise: any = $state();
let torrentsError: string | null = $state(null);
let isLoading: boolean = $state(false);
let filePathSuffix: string = $state('');
const tableColumnHeadings = [
{ name: 'Size', id: 'size' },
{ name: 'Usenet', id: 'usenet' },
{ name: 'Seeders', id: 'seeders' },
{ name: 'Age', id: 'age' },
{ name: 'Score', id: 'score' },
{ name: 'Indexer', id: 'indexer' },
{ name: 'Indexer Flags', id: 'flags' }
];
function torrentMatchesSelectedEpisodes(
torrentTitle: string,
selectedEpisodes: { seasonNumber: number; episodeNumber: number }[]
) {
const normalizedTitle = torrentTitle.toLowerCase();
return selectedEpisodes.some((ep) => {
const s = String(ep.seasonNumber).padStart(2, '0');
const e = String(ep.episodeNumber).padStart(2, '0');
const patterns = [
`s${s}e${e}`,
`${s}x${e}`,
`season ${ep.seasonNumber} episode ${ep.episodeNumber}`
];
return patterns.some((pattern) => normalizedTitle.includes(pattern));
});
}
async function search() {
if (!selectedEpisodeNumbers || selectedEpisodeNumbers.length === 0) {
toast.error('No episodes selected.');
return;
}
isLoading = true;
torrentsError = null;
torrentsPromise = Promise.all(
selectedEpisodeNumbers.map((ep) =>
client
.GET('/api/v1/tv/torrents', {
params: {
query: {
show_id: show.id!,
season_number: ep.seasonNumber,
episode_number: ep.episodeNumber
}
}
})
.then((r) => r?.data ?? [])
)
)
.then((results) => results.flat())
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then((allTorrents: any[]) =>
allTorrents.filter((torrent) =>
torrentMatchesSelectedEpisodes(torrent.title, selectedEpisodeNumbers)
)
)
.finally(() => (isLoading = false));
try {
await torrentsPromise;
} catch (error: any) {
console.error(error);
torrentsError = error.message || 'An error occurred while searching for torrents.';
toast.error(torrentsError);
}
}
async function downloadTorrent(result_id: string) {
const { response } = await client.POST('/api/v1/tv/torrents', {
params: {
query: {
show_id: show.id!,
public_indexer_result_id: result_id,
override_file_path_suffix: filePathSuffix === '' ? undefined : filePathSuffix
}
}
});
if (!response.ok) {
toast.error('Download failed.');
} else {
toast.success('Download started.');
}
await invalidateAll();
}
</script>
<DownloadDialogWrapper
bind:open={dialogueState}
{triggerText}
title="Download Selected Episodes"
description="Search and download torrents for selected episodes."
>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted-foreground">
Selected episodes:
<strong>
{selectedEpisodeNumbers.length > 0
? selectedEpisodeNumbers
.map(
(e) =>
`S${String(e.seasonNumber).padStart(2, '0')}E${String(e.episodeNumber).padStart(2, '0')}`
)
.join(', ')
: 'None'}
</strong>
</p>
<Button
class="w-fit"
disabled={isLoading || selectedEpisodeNumbers.length === 0}
onclick={search}
>
Search Torrents
</Button>
</div>
{#if torrentsError}
<div class="my-2 w-full text-center text-red-500">
An error occurred: {torrentsError}
</div>
{/if}
<TorrentTable {torrentsPromise} columns={tableColumnHeadings}>
{#snippet rowSnippet(torrent)}
<Table.Cell>{torrent.title}</Table.Cell>
<Table.Cell>
{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB
</Table.Cell>
<Table.Cell>{torrent.usenet}</Table.Cell>
<Table.Cell>{torrent.usenet ? 'N/A' : torrent.seeders}</Table.Cell>
<Table.Cell>
{torrent.age ? formatSecondsToOptimalUnit(torrent.age) : torrent.usenet ? 'N/A' : ''}
</Table.Cell>
<Table.Cell>{torrent.score}</Table.Cell>
<Table.Cell>{torrent.indexer ?? 'unknown'}</Table.Cell>
<Table.Cell>
{#if torrent.flags}
{#each torrent.flags as flag (flag)}
<Badge variant="outline">{flag}</Badge>
{/each}
{/if}
</Table.Cell>
<Table.Cell class="text-right">
<SelectFilePathSuffixDialog
bind:filePathSuffix
media={show}
callback={() => downloadTorrent(torrent.id)}
/>
</Table.Cell>
{/snippet}
</TorrentTable>
</DownloadDialogWrapper>

View File

@@ -0,0 +1,189 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { toast } from 'svelte-sonner';
import { formatSecondsToOptimalUnit } from '$lib/utils.ts';
import * as Table from '$lib/components/ui/table';
import { Badge } from '$lib/components/ui/badge';
import client from '$lib/api';
import type { components } from '$lib/api/api';
import SelectFilePathSuffixDialog from '$lib/components/download-dialogs/select-file-path-suffix-dialog.svelte';
import { invalidateAll } from '$app/navigation';
import TorrentTable from '$lib/components/download-dialogs/torrent-table.svelte';
import DownloadDialogWrapper from '$lib/components/download-dialogs/download-dialog-wrapper.svelte';
let {
show,
selectedSeasonNumbers,
triggerText = 'Download Selected Seasons'
}: {
show: components['schemas']['Show'];
selectedSeasonNumbers: number[];
triggerText?: string;
} = $props();
let dialogueState = $state(false);
let torrentsError: string | null = $state(null);
let filePathSuffix: string = $state('');
let torrentsPromise: any = $state();
let isLoading: boolean = $state(false);
const tableColumnHeadings = [
{ name: 'Size', id: 'size' },
{ name: 'Usenet', id: 'usenet' },
{ name: 'Seeders', id: 'seeders' },
{ name: 'Age', id: 'age' },
{ name: 'Score', id: 'score' },
{ name: 'Indexer', id: 'indexer' },
{ name: 'Indexer Flags', id: 'flags' },
{ name: 'Seasons', id: 'season' }
];
async function downloadTorrent(result_id: string) {
torrentsError = null;
const { response } = await client.POST('/api/v1/tv/torrents', {
params: {
query: {
show_id: show.id!,
public_indexer_result_id: result_id,
override_file_path_suffix: filePathSuffix === '' ? undefined : filePathSuffix
}
}
});
if (response.status === 409) {
const errorMessage = `Filepath Suffix '${filePathSuffix}' already exists.`;
torrentsError = errorMessage;
toast.error(errorMessage);
} else if (!response.ok) {
const errorMessage = `Failed to download torrent: ${response.statusText}`;
torrentsError = errorMessage;
toast.error(errorMessage);
} else {
toast.success('Torrent download started successfully!');
}
await invalidateAll();
}
function isEpisodeRelease(title: string) {
const lower = title.toLowerCase();
const episodePatterns = [
/s\d{1,2}e\d{1,2}/i,
/\d{1,2}x\d{1,2}/i,
/\be\d{1,2}\b/i,
/e\d{1,2}-e?\d{1,2}/i,
/vol\.?\s?\d+/i
];
return episodePatterns.some((regex) => regex.test(lower));
}
async function search() {
if (!selectedSeasonNumbers || selectedSeasonNumbers.length === 0) {
toast.error('No seasons selected.');
return;
}
isLoading = true;
torrentsError = null;
toast.info(`Searching torrents for seasons: ${selectedSeasonNumbers.join(', ')}`);
torrentsPromise = Promise.all(
selectedSeasonNumbers.map((seasonNumber) =>
client
.GET('/api/v1/tv/torrents', {
params: {
query: {
show_id: show.id!,
season_number: seasonNumber
}
}
})
.then((data) => data?.data ?? [])
)
)
.then((results) => results.flat())
.then((allTorrents) => allTorrents.filter((torrent) => !isEpisodeRelease(torrent.title)))
.finally(() => (isLoading = false));
try {
await torrentsPromise;
} catch (error: any) {
console.error(error);
torrentsError = error.message || 'An error occurred while searching for torrents.';
toast.error(torrentsError);
}
}
</script>
<DownloadDialogWrapper
bind:open={dialogueState}
{triggerText}
title="Download Selected Seasons"
description="Search and download torrents for the selected seasons."
>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted-foreground">
Selected seasons:
<strong>
{selectedSeasonNumbers.length > 0
? selectedSeasonNumbers
.slice()
.sort((a, b) => a - b)
.map((n) => `S${String(n).padStart(2, '0')}`)
.join(', ')
: 'None'}
</strong>
</p>
<Button
class="w-fit"
disabled={isLoading || selectedSeasonNumbers.length === 0}
onclick={search}
>
Search Torrents
</Button>
</div>
{#if torrentsError}
<div class="my-2 w-full text-center text-red-500">
An error occurred: {torrentsError}
</div>
{/if}
<TorrentTable {torrentsPromise} columns={tableColumnHeadings}>
{#snippet rowSnippet(torrent)}
<Table.Cell class="font-medium">{torrent.title}</Table.Cell>
<Table.Cell>
{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB
</Table.Cell>
<Table.Cell>{torrent.usenet}</Table.Cell>
<Table.Cell>{torrent.usenet ? 'N/A' : torrent.seeders}</Table.Cell>
<Table.Cell>
{torrent.age ? formatSecondsToOptimalUnit(torrent.age) : torrent.usenet ? 'N/A' : ''}
</Table.Cell>
<Table.Cell>{torrent.score}</Table.Cell>
<Table.Cell>{torrent.indexer ?? 'unknown'}</Table.Cell>
<Table.Cell>
{#if torrent.flags}
{#each torrent.flags as flag (flag)}
<Badge variant="outline">{flag}</Badge>
{/each}
{/if}
</Table.Cell>
<Table.Cell>
{torrent.season ?? '-'}
</Table.Cell>
<Table.Cell class="text-right">
<SelectFilePathSuffixDialog
bind:filePathSuffix
media={show}
callback={() => downloadTorrent(torrent.id)}
/>
</Table.Cell>
{/snippet}
</TorrentTable>
</DownloadDialogWrapper>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
convertTorrentSeasonRangeToIntegerRange, convertTorrentSeasonRangeToIntegerRange,
convertTorrentEpisodeRangeToIntegerRange,
getTorrentQualityString, getTorrentQualityString,
getTorrentStatusString getTorrentStatusString
} from '$lib/utils.js'; } from '$lib/utils.js';
@@ -59,6 +60,7 @@
<Table.Head>Name</Table.Head> <Table.Head>Name</Table.Head>
{#if isShow} {#if isShow}
<Table.Head>Seasons</Table.Head> <Table.Head>Seasons</Table.Head>
<Table.Head>Episodes</Table.Head>
{/if} {/if}
<Table.Head>Download Status</Table.Head> <Table.Head>Download Status</Table.Head>
<Table.Head>Quality</Table.Head> <Table.Head>Quality</Table.Head>
@@ -97,6 +99,11 @@
(torrent as components['schemas']['RichSeasonTorrent']).seasons! (torrent as components['schemas']['RichSeasonTorrent']).seasons!
)} )}
</Table.Cell> </Table.Cell>
<Table.Cell>
{convertTorrentEpisodeRangeToIntegerRange(
(torrent as components['schemas']['RichSeasonTorrent']).episodes!
)}
</Table.Cell>
{/if} {/if}
<Table.Cell> <Table.Cell>
{getTorrentStatusString(torrent.status)} {getTorrentStatusString(torrent.status)}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -51,6 +51,17 @@ export function convertTorrentSeasonRangeToIntegerRange(seasons: number[]): stri
} }
} }
export function convertTorrentEpisodeRangeToIntegerRange(episodes: number[]): string {
if (episodes.length === 1) return episodes[0]?.toString() || 'unknown';
else if (episodes.length > 1) {
const lastEpisode = episodes.at(-1);
return episodes[0]?.toString() + '-' + (lastEpisode?.toString() || 'unknown');
} else {
console.log('Error parsing episode range: ' + episodes);
return 'Error parsing episode range: ' + episodes;
}
}
export async function handleLogout() { export async function handleLogout() {
await client.POST('/api/v1/auth/cookie/logout'); await client.POST('/api/v1/auth/cookie/logout');
await goto(resolve('/login', {})); await goto(resolve('/login', {}));

View File

@@ -80,7 +80,7 @@
<Card.Title>Overview</Card.Title> <Card.Title>Overview</Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<p class="leading-7 not-first:mt-6"> <p class="text-justify text-sm leading-6 hyphens-auto text-muted-foreground">
{movie.overview} {movie.overview}
</p> </p>
</Card.Content> </Card.Content>

View File

@@ -4,11 +4,14 @@
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js'; import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { ImageOff } from 'lucide-svelte'; import { ImageOff } from 'lucide-svelte';
import { Ellipsis } from 'lucide-svelte';
import * as Table from '$lib/components/ui/table/index.js'; import * as Table from '$lib/components/ui/table/index.js';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import type { components } from '$lib/api/api'; import type { components } from '$lib/api/api';
import { getFullyQualifiedMediaName } from '$lib/utils'; import { getFullyQualifiedMediaName } from '$lib/utils';
import DownloadSeasonDialog from '$lib/components/download-dialogs/download-season-dialog.svelte'; import DownloadSelectedSeasonsDialog from '$lib/components/download-dialogs/download-selected-seasons-dialog.svelte';
import DownloadSelectedEpisodesDialog from '$lib/components/download-dialogs/download-selected-episodes-dialog.svelte';
import DownloadCustomDialog from '$lib/components/download-dialogs/download-custom-dialog.svelte';
import CheckmarkX from '$lib/components/checkmark-x.svelte'; import CheckmarkX from '$lib/components/checkmark-x.svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import TorrentTable from '$lib/components/torrents/torrent-table.svelte'; import TorrentTable from '$lib/components/torrents/torrent-table.svelte';
@@ -22,11 +25,85 @@
import DeleteMediaDialog from '$lib/components/delete-media-dialog.svelte'; import DeleteMediaDialog from '$lib/components/delete-media-dialog.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import client from '$lib/api'; import client from '$lib/api';
import { Checkbox } from '$lib/components/ui/checkbox';
import { SvelteSet } from 'svelte/reactivity';
let show: components['schemas']['PublicShow'] = $derived(page.data.showData); let show: components['schemas']['PublicShow'] = $derived(page.data.showData);
let torrents: components['schemas']['RichShowTorrent'] = $derived(page.data.torrentsData); let torrents: components['schemas']['RichShowTorrent'] = $derived(page.data.torrentsData);
let user: () => components['schemas']['UserRead'] = getContext('user'); let user: () => components['schemas']['UserRead'] = getContext('user');
let expandedSeasons = $state<Set<string>>(new Set());
function toggleSeason(seasonId: string) {
if (expandedSeasons.has(seasonId)) {
expandedSeasons.delete(seasonId);
} else {
expandedSeasons.add(seasonId);
}
expandedSeasons = new SvelteSet(expandedSeasons);
}
let selectedSeasons = $state<Set<string>>(new Set());
function toggleSeasonSelection(seasonId: string) {
if (selectedSeasons.has(seasonId)) {
selectedSeasons.delete(seasonId);
} else {
selectedSeasons.add(seasonId);
}
selectedSeasons = new SvelteSet(selectedSeasons);
}
let selectedSeasonNumbers = $derived(
show.seasons.filter((s) => selectedSeasons.has(s.id)).map((s) => s.number)
);
let downloadButtonLabel = $derived(
selectedSeasonNumbers.length === 0
? 'Download Seasons'
: `Download Season(s) ${selectedSeasonNumbers
.slice()
.sort((a, b) => a - b)
.map((n) => `S${String(n).padStart(2, '0')}`)
.join(', ')}`
);
let selectedEpisodes = $state<Set<string>>(new Set());
function toggleEpisodeSelection(episodeId: string) {
if (selectedEpisodes.has(episodeId)) {
selectedEpisodes.delete(episodeId);
} else {
selectedEpisodes.add(episodeId);
}
selectedEpisodes = new SvelteSet(selectedEpisodes);
}
let selectedEpisodeNumbers = $derived(
show.seasons.flatMap((season) =>
season.episodes
.filter((ep) => selectedEpisodes.has(ep.id))
.map((ep) => ({
seasonNumber: season.number,
episodeNumber: ep.number
}))
)
);
let episodeDownloadLabel = $derived(
selectedEpisodeNumbers.length === 0
? 'Download Episodes'
: `Download Episode(s) ${selectedEpisodeNumbers
.map(
(e) =>
`S${String(e.seasonNumber).padStart(2, '0')}E${String(e.episodeNumber).padStart(
2,
'0'
)}`
)
.join(', ')}`
);
let continuousDownloadEnabled = $derived(show.continuous_download); let continuousDownloadEnabled = $derived(show.continuous_download);
async function toggle_continuous_download() { async function toggle_continuous_download() {
@@ -109,7 +186,7 @@
<Card.Title>Overview</Card.Title> <Card.Title>Overview</Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<p class="leading-7 not-first:mt-6"> <p class="text-justify text-sm leading-6 hyphens-auto text-muted-foreground">
{show.overview} {show.overview}
</p> </p>
</Card.Content> </Card.Content>
@@ -146,7 +223,23 @@
</Card.Header> </Card.Header>
<Card.Content class="flex flex-col items-center gap-4"> <Card.Content class="flex flex-col items-center gap-4">
{#if user().is_superuser} {#if user().is_superuser}
<DownloadSeasonDialog {show} /> {#if selectedSeasonNumbers.length > 0}
<DownloadSelectedSeasonsDialog
{show}
{selectedSeasonNumbers}
triggerText={downloadButtonLabel}
/>
{/if}
{#if selectedEpisodeNumbers.length > 0}
<DownloadSelectedEpisodesDialog
{show}
{selectedEpisodeNumbers}
triggerText={episodeDownloadLabel}
/>
{/if}
{#if selectedSeasonNumbers.length === 0 && selectedEpisodeNumbers.length === 0}
<DownloadCustomDialog {show} />
{/if}
{/if} {/if}
<RequestSeasonDialog {show} /> <RequestSeasonDialog {show} />
</Card.Content> </Card.Content>
@@ -162,35 +255,87 @@
</Card.Description> </Card.Description>
</Card.Header> </Card.Header>
<Card.Content class="w-full overflow-x-auto"> <Card.Content class="w-full overflow-x-auto">
<Table.Root> <Table.Root class="w-full table-fixed">
<Table.Caption>A list of all seasons.</Table.Caption> <Table.Caption>A list of all seasons.</Table.Caption>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.Head>Number</Table.Head> <Table.Head class="w-[40px]"></Table.Head>
<Table.Head>Exists on file</Table.Head> <Table.Head class="w-[80px]">Number</Table.Head>
<Table.Head>Title</Table.Head> <Table.Head class="w-[100px]">Exists on file</Table.Head>
<Table.Head class="w-[240px]">Title</Table.Head>
<Table.Head>Overview</Table.Head> <Table.Head>Overview</Table.Head>
<Table.Head class="w-[64px] text-center">Details</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#if show.seasons.length > 0} {#if show.seasons.length > 0}
{#each show.seasons as season (season.id)} {#each show.seasons as season (season.id)}
<Table.Row <Table.Row
onclick={() => class={`group cursor-pointer transition-colors hover:bg-muted/60 ${
goto( expandedSeasons.has(season.id) ? 'bg-muted/50' : 'bg-muted/10'
resolve('/dashboard/tv/[showId]/[seasonId]', { }`}
showId: show.id, onclick={() => toggleSeason(season.id)}
seasonId: season.id
})
)}
> >
<Table.Cell class="min-w-[10px] font-medium">{season.number}</Table.Cell> <Table.Cell class="w-[40px]">
<Checkbox
checked={selectedSeasons.has(season.id)}
onCheckedChange={() => toggleSeasonSelection(season.id)}
onclick={(e) => e.stopPropagation()}
/>
</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium">
S{String(season.number).padStart(2, '0')}
</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium"> <Table.Cell class="min-w-[10px] font-medium">
<CheckmarkX state={season.downloaded} /> <CheckmarkX state={season.downloaded} />
</Table.Cell> </Table.Cell>
<Table.Cell class="min-w-[50px]">{season.name}</Table.Cell> <Table.Cell class="min-w-[50px]">{season.name}</Table.Cell>
<Table.Cell class="max-w-[300px] truncate">{season.overview}</Table.Cell> <Table.Cell class="max-w-[300px] truncate">{season.overview}</Table.Cell>
<Table.Cell class="w-[64px] text-center">
<button
class="inline-flex cursor-pointer items-center
justify-center
rounded-md p-1
transition-colors
hover:bg-muted/95
focus-visible:ring-2
focus-visible:ring-ring focus-visible:outline-none"
onclick={(e) => {
e.stopPropagation();
goto(
resolve('/dashboard/tv/[showId]/[seasonId]', {
showId: show.id,
seasonId: season.id
})
);
}}
aria-label="Season details"
>
<Ellipsis size={16} class="text-muted-foreground" />
</button>
</Table.Cell>
</Table.Row> </Table.Row>
{#if expandedSeasons.has(season.id)}
{#each season.episodes as episode (episode.id)}
<Table.Row class="bg-muted/20">
<Table.Cell class="w-[40px]">
<Checkbox
checked={selectedEpisodes.has(episode.id)}
onCheckedChange={() => toggleEpisodeSelection(episode.id)}
onclick={(e) => e.stopPropagation()}
/>
</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium">
E{String(episode.number).padStart(2, '0')}
</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium">
<CheckmarkX state={episode.downloaded} />
</Table.Cell>
<Table.Cell class="min-w-[50px]">{episode.title}</Table.Cell>
<Table.Cell colspan={2} class="truncate">{episode.overview}</Table.Cell>
</Table.Row>
{/each}
{/if}
{/each} {/each}
{:else} {:else}
<Table.Row> <Table.Row>

View File

@@ -11,9 +11,15 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '$lib/components/ui/card/index.js';
let seasonFiles: components['schemas']['PublicSeasonFile'][] = $derived(page.data.files); let episodeFiles: components['schemas']['PublicEpisodeFile'][] = $derived(page.data.files);
let season: components['schemas']['Season'] = $derived(page.data.season); let season: components['schemas']['Season'] = $derived(page.data.season);
let show: components['schemas']['Show'] = $derived(page.data.showData); let show: components['schemas']['Show'] = $derived(page.data.showData);
let episodeById = $derived(
Object.fromEntries(
season.episodes.map((ep) => [ep.id, `E${String(ep.number).padStart(2, '0')}`])
)
);
</script> </script>
<svelte:head> <svelte:head>
@@ -59,7 +65,7 @@
</div> </div>
</header> </header>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl"> <h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
{getFullyQualifiedMediaName(show)} Season {season.number} {getFullyQualifiedMediaName(show)} - Season {season.number}
</h1> </h1>
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]"> <main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<div class="flex flex-col gap-4 md:flex-row md:items-stretch"> <div class="flex flex-col gap-4 md:flex-row md:items-stretch">
@@ -68,13 +74,20 @@
</div> </div>
<div class="h-full w-full flex-auto rounded-xl md:w-1/4"> <div class="h-full w-full flex-auto rounded-xl md:w-1/4">
<Card.Root class="h-full w-full"> <Card.Root class="h-full w-full">
<Card.Header> <Card.Content class="flex flex-col gap-6">
<Card.Title>Overview</Card.Title> <div>
</Card.Header> <Card.Title class="mb-2 text-base">Series Overview</Card.Title>
<Card.Content> <p class="text-justify text-sm leading-6 hyphens-auto text-muted-foreground">
<p class="leading-7 not-first:mt-6"> {show.overview}
{show.overview} </p>
</p> </div>
<div class="border-t border-border"></div>
<div>
<Card.Title class="mb-2 text-base">Season Overview</Card.Title>
<p class="text-justify text-sm leading-6 hyphens-auto text-muted-foreground">
{season.overview}
</p>
</div>
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>
@@ -95,14 +108,18 @@
> >
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.Head>Episode</Table.Head>
<Table.Head>Quality</Table.Head> <Table.Head>Quality</Table.Head>
<Table.Head>File Path Suffix</Table.Head> <Table.Head>File Path Suffix</Table.Head>
<Table.Head>Imported</Table.Head> <Table.Head>Imported</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each seasonFiles as file (file)} {#each episodeFiles as file (file)}
<Table.Row> <Table.Row>
<Table.Cell class="w-[50px]">
{episodeById[file.episode_id] ?? 'E??'}
</Table.Cell>
<Table.Cell class="w-[50px]"> <Table.Cell class="w-[50px]">
{getTorrentQualityString(file.quality)} {getTorrentQualityString(file.quality)}
</Table.Cell> </Table.Cell>
@@ -114,7 +131,11 @@
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
{:else} {:else}
<span class="font-semibold">You haven't downloaded this season yet.</span> <Table.Row>
<Table.Cell colspan={4} class="text-center py-6 font-semibold">
You haven't downloaded episodes of this season yet.
</Table.Cell>
</Table.Row>
{/each} {/each}
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
@@ -132,19 +153,23 @@
</Card.Description> </Card.Description>
</Card.Header> </Card.Header>
<Card.Content class="w-full overflow-x-auto"> <Card.Content class="w-full overflow-x-auto">
<Table.Root> <Table.Root class="w-full table-fixed">
<Table.Caption>A list of all episodes.</Table.Caption> <Table.Caption>A list of all episodes.</Table.Caption>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.Head class="w-[100px]">Number</Table.Head> <Table.Head class="w-[80px]">Number</Table.Head>
<Table.Head class="min-w-[50px]">Title</Table.Head> <Table.Head class="w-[240px]">Title</Table.Head>
<Table.Head>Overview</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each season.episodes as episode (episode.id)} {#each season.episodes as episode (episode.id)}
<Table.Row> <Table.Row>
<Table.Cell class="w-[100px] font-medium">{episode.number}</Table.Cell> <Table.Cell class="w-[100px] font-medium"
>E{String(episode.number).padStart(2, '0')}</Table.Cell
>
<Table.Cell class="min-w-[50px]">{episode.title}</Table.Cell> <Table.Cell class="min-w-[50px]">{episode.title}</Table.Cell>
<Table.Cell class="truncate">{episode.overview}</Table.Cell>
</Table.Row> </Table.Row>
{/each} {/each}
</Table.Body> </Table.Body>

View File

@@ -10,7 +10,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
} }
} }
}); });
const seasonFiles = client.GET('/api/v1/tv/seasons/{season_id}/files', { const episodeFiles = client.GET('/api/v1/tv/seasons/{season_id}/files', {
fetch: fetch, fetch: fetch,
params: { params: {
path: { path: {
@@ -19,7 +19,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
} }
}); });
return { return {
files: await seasonFiles.then((x) => x.data), files: await episodeFiles.then((x) => x.data),
season: await season.then((x) => x.data) season: await season.then((x) => x.data)
}; };
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 110 KiB