Showing preview only (1,599K chars total). Download the full file or copy to clipboard to get everything.
Repository: ShokoAnime/Shokofin
Branch: dev
Commit: e9d362945367
Files: 201
Total size: 1.5 MB
Directory structure:
gitextract_n8jd1_7_/
├── .config/
│ └── dotnet-tools.json
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ └── features.yml
│ └── workflows/
│ ├── changelog.jq
│ ├── git-log-json.mjs
│ ├── release-daily.yml
│ ├── release.yml
│ └── release_draft.jq
├── .gitignore
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── LICENSE
├── README.md
├── Shokofin/
│ ├── API/
│ │ ├── Converters/
│ │ │ └── JsonAutoStringConverter.cs
│ │ ├── IdPrefix.cs
│ │ ├── Info/
│ │ │ ├── AniDB/
│ │ │ │ ├── AnidbAnimeInfo.cs
│ │ │ │ └── AnidbEpisodeInfo.cs
│ │ │ ├── CollectionInfo.cs
│ │ │ ├── EpisodeInfo.cs
│ │ │ ├── FileInfo.cs
│ │ │ ├── IBaseItemInfo.cs
│ │ │ ├── IExtendedItemInfo.cs
│ │ │ ├── SeasonInfo.cs
│ │ │ ├── Shoko/
│ │ │ │ ├── ShokoEpisodeInfo.cs
│ │ │ │ └── ShokoSeriesInfo.cs
│ │ │ ├── ShowInfo.cs
│ │ │ └── TMDB/
│ │ │ ├── TmdbEpisodeInfo.cs
│ │ │ ├── TmdbMovieInfo.cs
│ │ │ ├── TmdbSeasonInfo.cs
│ │ │ └── TmdbShowInfo.cs
│ │ ├── Models/
│ │ │ ├── AniDB/
│ │ │ │ ├── AnidbAnime.cs
│ │ │ │ └── AnidbEpisode.cs
│ │ │ ├── ApiException.cs
│ │ │ ├── ApiKey.cs
│ │ │ ├── ComponentVersion.cs
│ │ │ ├── ContentRating.cs
│ │ │ ├── CrossReference.cs
│ │ │ ├── EpisodeType.cs
│ │ │ ├── File.cs
│ │ │ ├── IDs.cs
│ │ │ ├── Image.cs
│ │ │ ├── Images.cs
│ │ │ ├── ListResult.cs
│ │ │ ├── ManagedFolder.cs
│ │ │ ├── Rating.cs
│ │ │ ├── Relation.cs
│ │ │ ├── ReleaseGroup.cs
│ │ │ ├── ReleaseInfo.cs
│ │ │ ├── ReleaseSource.cs
│ │ │ ├── Role.cs
│ │ │ ├── SeriesType.cs
│ │ │ ├── Shoko/
│ │ │ │ ├── ShokoEpisode.cs
│ │ │ │ ├── ShokoGroup.cs
│ │ │ │ └── ShokoSeries.cs
│ │ │ ├── Studio.cs
│ │ │ ├── TMDB/
│ │ │ │ ├── AlternateOrderingType.cs
│ │ │ │ ├── ITmdbEntity.cs
│ │ │ │ ├── ITmdbParentEntity.cs
│ │ │ │ ├── TmdbEpisode.cs
│ │ │ │ ├── TmdbEpisodeCrossReference.cs
│ │ │ │ ├── TmdbMovie.cs
│ │ │ │ ├── TmdbMovieCollection.cs
│ │ │ │ ├── TmdbMovieCrossReference.cs
│ │ │ │ ├── TmdbSeason.cs
│ │ │ │ └── TmdbShow.cs
│ │ │ ├── Tag.cs
│ │ │ ├── Text.cs
│ │ │ ├── Title.cs
│ │ │ ├── TitleType.cs
│ │ │ ├── YearlySeason.cs
│ │ │ └── YearlySeasonName.cs
│ │ ├── ShokoApiClient.cs
│ │ ├── ShokoApiManager.cs
│ │ └── ShokoIdLookup.cs
│ ├── Collections/
│ │ └── CollectionManager.cs
│ ├── Configuration/
│ │ ├── AllDescriptionsConfiguration.cs
│ │ ├── AllImagesConfiguration.cs
│ │ ├── AllTitlesConfiguration.cs
│ │ ├── DebugConfiguration.cs
│ │ ├── DescriptionConfiguration.cs
│ │ ├── Enums/
│ │ │ ├── ImageLanguageType.cs
│ │ │ ├── MetadataRefreshField.cs
│ │ │ ├── SeasonMergingBehavior.cs
│ │ │ ├── SeriesEpisodeConversion.cs
│ │ │ ├── SeriesStructureType.cs
│ │ │ └── VirtualRootLocation.cs
│ │ ├── ImageConfiguration.cs
│ │ ├── LegacyMediaFolderConfiguration.cs
│ │ ├── LibraryConfiguration.cs
│ │ ├── MediaFolderConfiguration.cs
│ │ ├── MetadataRefreshConfiguration.cs
│ │ ├── Models/
│ │ │ ├── LibraryConfigurationChangedEventArgs.cs
│ │ │ └── MediaFolderConfigurationChangedEventArgs.cs
│ │ ├── PluginConfiguration.cs
│ │ ├── SeriesConfiguration.cs
│ │ ├── Services/
│ │ │ ├── MediaFolderConfigurationService.cs
│ │ │ └── SeriesConfigurationService.cs
│ │ ├── TitleConfiguration.cs
│ │ ├── TitlesConfiguration.cs
│ │ └── UserConfiguration.cs
│ ├── Events/
│ │ ├── EventDispatchService.cs
│ │ ├── Interfaces/
│ │ │ ├── IFileEventArgs.cs
│ │ │ ├── IFileRelocationEventArgs.cs
│ │ │ ├── IMetadataUpdatedEventArgs.cs
│ │ │ ├── IReleaseSavedEventArgs.cs
│ │ │ ├── ProviderName.cs
│ │ │ └── UpdateReason.cs
│ │ ├── MetadataRefreshService.cs
│ │ └── Stub/
│ │ └── FileEventArgsStub.cs
│ ├── Extensions/
│ │ ├── CollectionTypeExtensions.cs
│ │ ├── EnumerableExtensions.cs
│ │ ├── EpisodeTypeExtensions.cs
│ │ ├── ListExtensions.cs
│ │ ├── MediaFolderConfigurationExtensions.cs
│ │ ├── StringExtensions.cs
│ │ └── SyncExtensions.cs
│ ├── ExternalIds/
│ │ ├── AnidbAnimeId.cs
│ │ ├── AnidbCreatorId.cs
│ │ ├── AnidbEpisodeId.cs
│ │ ├── ProviderNames.cs
│ │ ├── ProviderUrls.cs
│ │ ├── ShokoExternalUrlHandler.cs
│ │ └── ShokoInternalId.cs
│ ├── MergeVersions/
│ │ ├── MergeVersionManager.cs
│ │ └── MergeVersionSortSelector.cs
│ ├── Pages/
│ │ ├── Dummy.html
│ │ ├── Scripts/
│ │ │ ├── Common.js
│ │ │ ├── Dummy.js
│ │ │ ├── Settings.js
│ │ │ └── jsconfig.json
│ │ └── Settings.html
│ ├── Plugin.cs
│ ├── PluginServiceRegistrator.cs
│ ├── Providers/
│ │ ├── BoxSetProvider.cs
│ │ ├── CustomBoxSetProvider.cs
│ │ ├── CustomEpisodeProvider.cs
│ │ ├── CustomMovieProvider.cs
│ │ ├── CustomSeasonProvider.cs
│ │ ├── CustomSeriesProvider.cs
│ │ ├── EpisodeProvider.cs
│ │ ├── ImageProvider.cs
│ │ ├── MovieProvider.cs
│ │ ├── SeasonProvider.cs
│ │ ├── SeriesProvider.cs
│ │ ├── TrailerProvider.cs
│ │ └── VideoProvider.cs
│ ├── Resolvers/
│ │ ├── Models/
│ │ │ ├── LinkGenerationResult.cs
│ │ │ └── ShokoWatcher.cs
│ │ ├── ShokoIgnoreRule.cs
│ │ ├── ShokoLibraryMonitor.cs
│ │ ├── ShokoResolver.cs
│ │ └── VirtualFileSystemService.cs
│ ├── Shokofin.csproj
│ ├── SignalR/
│ │ ├── Models/
│ │ │ ├── EpisodeInfoUpdatedEventArgs.cs
│ │ │ ├── FileEventArgs.cs
│ │ │ ├── FileMovedEventArgs.cs
│ │ │ ├── FileRenamedEventArgs.cs
│ │ │ ├── MovieInfoUpdatedEventArgs.cs
│ │ │ ├── ReleaseSavedEventArgs.cs
│ │ │ └── SeriesInfoUpdatedEventArgs.cs
│ │ ├── SignalRConnectionManager.cs
│ │ ├── SignalREntryPoint.cs
│ │ └── SignalRRetryPolicy.cs
│ ├── Sync/
│ │ ├── SyncDirection.cs
│ │ └── UserDataSyncManager.cs
│ ├── Tasks/
│ │ ├── AutoRefreshMetadataTask.cs
│ │ ├── CleanupVirtualRootTask.cs
│ │ ├── ClearPluginCacheTask.cs
│ │ ├── ExportUserDataTask.cs
│ │ ├── ImportUserDataTask.cs
│ │ ├── MergeEpisodesTask.cs
│ │ ├── MergeMoviesTask.cs
│ │ ├── PostScanTask.cs
│ │ ├── ReconstructCollectionsTask.cs
│ │ ├── SplitEpisodesTask.cs
│ │ ├── SplitMoviesTask.cs
│ │ ├── SyncUserDataTask.cs
│ │ └── VersionCheckTask.cs
│ ├── Utils/
│ │ ├── ContentRating.cs
│ │ ├── DisposableAction.cs
│ │ ├── GuardedMemoryCache.cs
│ │ ├── IgnorePatterns.cs
│ │ ├── ImageUtility.cs
│ │ ├── LibraryScanWatcher.cs
│ │ ├── Ordering.cs
│ │ ├── PropertyWatcher.cs
│ │ ├── SeriesInfoRelationComparer.cs
│ │ ├── TagFilter.cs
│ │ ├── TextUtility.cs
│ │ └── UsageTracker.cs
│ └── Web/
│ ├── ImageHostUrl.cs
│ ├── Models/
│ │ ├── SimpleSeries.cs
│ │ └── VfsLibraryPreview.cs
│ ├── ShokofinHostController.cs
│ ├── ShokofinSignalRController.cs
│ ├── ShokofinUtilityController.cs
│ └── VfsActionFilter.cs
├── Shokofin.sln
├── build.yaml
├── build_plugin.py
└── manifest.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .config/dotnet-tools.json
================================================
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.9",
"commands": [
"dotnet-ef"
]
}
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: Shokofin Bug Report 101
description: Report any bugs here!
labels: []
projects: []
assignees: []
body:
- type: markdown
attributes:
value: |
## Shokofin Bug Report
**Important:** This form is exclusively for reporting bugs. If your issue is not due to a bug but you requires assistance (e.g. with setup) or if you just have a question or inquiry, then please seek help on our [Discord](https://discord.gg/shokoanime) server instead. Our Discord community is eager to assist, and we often respond faster and can provide more immediate support on Discord.
To help us understand and resolve your bug report more efficiently, please fill out the following information.
And remember, for quicker assistance on any inquiries, Discord is the way to go!
- type: input
id: jelly
attributes:
label: Jellyfin version.
placeholder: "E.g. `10.8.12`"
validations:
required: true
- type: input
id: shokofin
attributes:
label: Shokofin version.
placeholder: "E.g. `3.0.1.0`"
validations:
required: true
- type: input
id: Shokoserver
attributes:
label: Shoko Server version, release channel, and commit hash.
placeholder: "E.g. `1.0.0 Stable` or `1.0.0 Dev (efefefe)`"
validations:
required: true
- type: textarea
id: fileStructure
attributes:
label: File structure of your _Media Library Folder in Jellyfin_/_Import Folder in Shoko Server_.
placeholder: "E.g. ../Anime A/Episode 1.avi or ../Anime A/Season 1/Episode 1.avi"
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: Screenshot of the "library settings" section of the plugin settings.
validations:
required: true
- type: markdown
attributes:
value: |
Library type and metadata/image providers enabled for the library/libaries in Jellyfin.
- type: dropdown
id: library
attributes:
label: Library Type(s).
multiple: true
options:
- Shows
- Movies
- Movies & Shows
validations:
required: true
- type: checkboxes
id: metadataCheck
attributes:
label: "Do the issue persists after creating a library with Shoko set as the only metadata provider? (Now is your time to check if you haven't already.)"
options:
- label: "Yes, I hereby confirm that the issue persists after creating a library with Shoko set as the only metadata provider."
required: true
validations:
required: true
- type: textarea
id: issue
attributes:
label: Issue
description: Try to explain your issue in simple terms. We'll ask for details if it's needed.
validations:
required: true
- type: textarea
id: stackTrace
attributes:
label: Stack Trace
description: If relevant, paste here.
================================================
FILE: .github/ISSUE_TEMPLATE/features.yml
================================================
name: Shokofin Feature Request 101
description: Request your features here!
labels: []
projects: []
assignees: []
body:
- type: markdown
attributes:
value: |
**Feature Request**
Suggest a request or idea that will help the project!
- type: textarea
id: description
attributes:
label: Description
description: Please describe the feature you would like to request.
placeholder: Describe your feature here.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Suggested Solution
description: How would you like the feature to be implemented?
placeholder: Describe your solution here.
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Information
description: Any additional information you would like to provide?
placeholder: Provide any additional information here.
================================================
FILE: .github/workflows/changelog.jq
================================================
reduce .[] as $commit (
"";
. +
"### `\($commit.type)`: \($commit.subject). (\($commit.commit)) @\($commit.author.github) (date: \($commit.author.date), TZ: \($commit.author.timeZone))" +
if $commit.isSkipCI then " (_Skip CI_)" else "" end +
if $commit.body != null and $commit.body != "" then
"\n\n\($commit.body | gsub("\n"; "\n"))"
else
""
end +
"\n\n"
)
================================================
FILE: .github/workflows/git-log-json.mjs
================================================
#! /bin/env node
import { dirname, join } from "node:path";
import { execSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import process from "node:process";
// https://git-scm.com/docs/pretty-formats/2.21.0
// Get the range or hash from the command line arguments
const RangeOrHash = process.argv[2] || "";
// Form the git log command
const GitLogCommandBase = `git log ${RangeOrHash}`;
const EndingMarkers = new Set([
".",
",",
"!",
"?",
]);
const Placeholders = {
"H": "commit",
"P": "parents",
"T": "tree",
"s": "subject",
"b": "body",
"an": "author_name",
"ae": "author_email",
"aI": "author_date",
"cn": "committer_name",
"ce": "committer_email",
"cI": "committer_date",
};
const mappingUrl = import.meta.url.startsWith("file:")
? join(dirname(import.meta.url.slice(5)), "email-to-github.json")
: null;
const emailToGithubMapping = mappingUrl && existsSync(mappingUrl)
? JSON.parse(readFileSync(mappingUrl, "utf-8"))
: {};
const commitOrder = [];
const commits = {};
for (const [placeholder, name] of Object.entries(Placeholders)) {
const gitCommand = `${GitLogCommandBase} --format="%H2>>>>> %${placeholder}"`;
const output = execSync(gitCommand).toString();
const lines = output.split(/\r\n|\r|\n/g);
let commitId = "";
for (const line of lines) {
const match = line.match(/^([0-9a-f]{40})2>>>>> /);
if (match) {
commitId = match[1];
if (!commits[commitId]) {
commitOrder.push(commitId);
commits[commitId] = {};
}
// Handle multiple parent hashes
if (name === "parents") {
commits[commitId][name] = line.substring(match[0].length).trim().split(" ");
}
else {
commits[commitId][name] = line.substring(match[0].length).trimEnd();
}
}
else if (commitId) {
if (name === "parents") {
const commits = line.trim().split(" ").filter(l => l);
if (commits.length)
commits[commitId][name].push(...commits);
}
else {
commits[commitId][name] += "\n" + line.trimEnd();
}
}
}
}
// Add file-level changes to each commit
for (const commitId of commitOrder) {
try {
const fileStatusOutput = execSync(`git diff --name-status ${commitId}^ ${commitId}`).toString();
const lineChangesOutput = execSync(`git diff --numstat ${commitId}^ ${commitId}`).toString();
const files = [];
const fileStatusLines = fileStatusOutput.split(/\r\n|\r|\n/g).filter(a => a);
const lineChangesLines = lineChangesOutput.split(/\r\n|\r|\n/g).filter(a => a);
for (const [index, line] of fileStatusLines.entries()) {
const [rawStatus, path] = line.split(/\t/);
const status = rawStatus === "M" ?
"modified"
: rawStatus === "A" ?
"added"
: rawStatus === "D" ?
"deleted"
: rawStatus === "R" ?
"renamed"
: "untracked";
const lineChangeParts = lineChangesLines[index].split(/\t/);
const addedLines = parseInt(lineChangeParts[0] || "0", 10);
const removedLines = parseInt(lineChangeParts[1] || "0", 10);
files.push({
path,
status,
addedLines,
removedLines,
});
}
commits[commitId].files = files;
}
catch (error) {
commits[commitId].files = [];
}
}
// Trim trailing newlines from all values in the commits object
for (const commit of Object.values(commits)) {
for (const key in commit) {
if (typeof commit[key] === "string") {
commit[key] = commit[key].trimEnd();
}
}
}
// Convert commits object to a list of values
const commitsList = commitOrder.reverse()
.map((commitId) => commits[commitId])
.map(({ commit, parents, tree, subject, body, author_name, author_email, author_date, committer_name, committer_email, committer_date, files }) => ({
commit,
parents,
tree,
subject: /^\s*\w+\s*: ?/i.test(subject) ? subject.split(":").slice(1).join(":").trim() : subject.trim(),
type: /^\s*\w+\s*: ?/i.test(subject) ?
subject.split(":")[0].toLowerCase()
: subject.startsWith("Partially revert ") ?
"revert"
: parents.length > 1 ?
"merge"
: /^fix/i.test(subject) ?
"fix"
: "misc",
body,
author: {
name: author_name,
email: author_email,
github: emailToGithubMapping[author_email] || null,
date: new Date(author_date).toISOString(),
timeZone: author_date.substring(19) === "Z" ? "+00:00" : author_date.substring(19),
},
committer: {
name: committer_name,
email: committer_email,
github: emailToGithubMapping[committer_email] || null,
date: new Date(committer_date).toISOString(),
timeZone: committer_date.substring(19) === "Z" ? "+00:00" : committer_date.substring(19),
},
files,
}))
.map((commit) => ({
...commit,
subject: commit.subject.replace(/\[(?:skip|no) *ci\]/ig, "").trim().replace(/[\.:]+^/, ""),
body: commit.body ? commit.body.replace(/\[(?:skip|no) *ci\]/ig, "").trimEnd() : commit.body,
isSkipCI: /\[(?:skip|no) *ci\]/i.test(commit.subject) || Boolean(commit.body && /\[(?:skip|no) *ci\]/i.test(commit.body)),
type: commit.type == "feature" ? "feat" : commit.type === "refacor" ? "refactor" : commit.type == "mics" ? "misc" : commit.type,
}))
.map((commit) => ({
...commit,
subject: ((subject) => {
subject = (/[a-z]/.test(subject[0]) ? subject[0].toUpperCase() + subject.slice(1) : subject).trim();
if (subject.length > 0 && EndingMarkers.has(subject[subject.length - 1]))
subject = subject.slice(0, subject.length - 1);
return subject;
})(commit.subject),
}))
.filter((commit) => !(commit.type === "misc" && (commit.subject === "update unstable manifest" || commit.subject === "Update repo manifest" || commit.subject === "Update unstable repo manifest")))
.map((commit, index) => ({
...commit,
simple_type: ["misc", "refactor"].includes(commit.type) ? "change" : commit.type === "chore" ? "repo" : commit.type,
index,
}));
process.stdout.write(JSON.stringify(commitsList, null, 2));
================================================
FILE: .github/workflows/release-daily.yml
================================================
name: Build & Publish Dev Release
on:
push:
branches:
- dev
jobs:
current_info:
runs-on: ubuntu-latest
name: Current Information
outputs:
version: ${{ steps.release_info.outputs.version }}
tag: ${{ steps.release_info.outputs.tag }}
date: ${{ steps.commit_date_iso8601.outputs.date }}
sha: ${{ github.sha }}
sha_short: ${{ steps.commit_info.outputs.sha }}
changelog: ${{ steps.generate_changelog.outputs.CHANGELOG }}
steps:
- name: Checkout master
uses: actions/checkout@master
with:
ref: "${{ github.ref }}"
fetch-depth: 0 # This is set to download the full git history for the repo
- name: Get Previous Version
id: previous_release_info
uses: revam/gh-action-get-tag-and-version@v1
with:
branch: false
prefix: "v"
prefixRegex: "[vV]?"
suffixRegex: "dev"
suffix: "dev"
- name: Get Current Version
id: release_info
uses: revam/gh-action-get-tag-and-version@v1
with:
branch: false
increment: suffix
prefix: "v"
prefixRegex: "[vV]?"
suffixRegex: "dev"
suffix: "dev"
- name: Get Commit Date (as ISO8601)
id: commit_date_iso8601
shell: bash
run: |
echo "date=$(git --no-pager show -s --format=%aI ${{ github.sha }})" >> "$GITHUB_OUTPUT"
- id: commit_info
name: Shorten Commit Hash
uses: actions/github-script@v6
with:
script: |
const sha = context.sha.substring(0, 7);
core.setOutput("sha", sha);
- name: Generate Changelog
id: generate_changelog
env:
PREVIOUS_COMMIT: ${{ steps.previous_release_info.outputs.commit }}
NEXT_COMMIT: ${{ github.sha }}
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "CHANGELOG<<$EOF" >> "$GITHUB_OUTPUT"
node .github/workflows/git-log-json.mjs $PREVIOUS_COMMIT..$NEXT_COMMIT | jq -r '.[] | "\n`\(.type)`: **\(.subject)**" + if .body != null and .body != "" then if .isSkipCI then ": (_Skip CI_)\n\n\(.body)" else ":\n\n\(.body)" end else if .isSkipCI then ". (_Skip CI_)" else "." end end' >> "$GITHUB_OUTPUT"
echo -e "\n$EOF" >> "$GITHUB_OUTPUT"
build_plugin:
runs-on: ubuntu-latest
needs:
- current_info
name: Build & Release (Dev)
steps:
- name: Checkout
uses: actions/checkout@master
with:
ref: ${{ github.ref }}
fetch-depth: 0 # This is set to download the full git history for the repo
- name: Fetch Dev Manifest from Metadata Branch
run: |
git checkout origin/metadata -- dev/manifest.json;
git reset;
rm manifest.json;
mv dev/manifest.json manifest.json;
rmdir dev;
- name: Setup .Net
uses: actions/setup-dotnet@v1
with:
dotnet-version: 9.0.x
- name: Restore Nuget Packages
run: dotnet restore Shokofin/Shokofin.csproj
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install JPRM
run: python -m pip install jprm
- name: Run JPRM
env:
CHANGELOG: ${{ needs.current_info.outputs.changelog }}
run: python build_plugin.py --repo ${{ github.repository }} --version=${{ needs.current_info.outputs.version }} --tag=${{ needs.current_info.outputs.tag }} --prerelease=True
- name: Change to Metadata Branch
run: |
mkdir dev;
mv manifest.json dev
git add ./dev/manifest.json;
git stash push --staged --message "Temp release details";
git reset --hard;
git checkout origin/metadata -B metadata;
git stash apply || git checkout --theirs dev/manifest.json;
git reset;
- name: Create Pre-Release
uses: softprops/action-gh-release@v1
with:
files: ./artifacts/shoko_*.zip
name: "Shokofin Dev ${{ needs.current_info.outputs.version }}"
tag_name: ${{ needs.current_info.outputs.tag }}
body: |
Update your plugin using the [dev manifest](https://raw.githubusercontent.com/ShokoAnime/Shokofin/metadata/dev/manifest.json) or by downloading the release from [GitHub Releases](https://github.com/ShokoAnime/Shokofin/releases/tag/${{ needs.current_info.outputs.tag }}) and installing it manually!
**Changes since last build**:
${{ needs.current_info.outputs.changelog }}
prerelease: true
fail_on_unmatched_files: true
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update Dev Manifest
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: metadata
commit_message: "misc: update dev manifest"
file_pattern: dev/manifest.json
skip_fetch: true
discord-notify:
runs-on: ubuntu-latest
name: Send notifications about the new daily build
needs:
- current_info
- build_plugin
steps:
- name: Notify Discord Users
uses: tsickert/discord-webhook@v6.0.0
if: contains(env.DISCORD_WEBHOOK, 'https://')
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
webhook-url: ${{ env.DISCORD_WEBHOOK }}
embed-color: 9985983
embed-timestamp: ${{ needs.current_info.outputs.date }}
embed-author-name: Shokofin | New Dev Build
embed-author-icon-url: https://raw.githubusercontent.com/${{ github.repository }}/dev/.github/images/jellyfin.png
embed-author-url: https://github.com/${{ github.repository }}
embed-description: |
**Version**: `${{ needs.current_info.outputs.version }}` (`${{ needs.current_info.outputs.sha_short }}`)
Update your plugin using the [dev manifest](https://raw.githubusercontent.com/${{ github.repository }}/metadata/dev/manifest.json) or by downloading the release from [GitHub Releases](https://github.com/${{ github.repository }}/releases/tag/${{ needs.current_info.outputs.tag }}) and installing it manually!
**Changes since last build**:
${{ needs.current_info.outputs.changelog }}
================================================
FILE: .github/workflows/release.yml
================================================
name: Build Stable Release
on:
release:
types:
- released
jobs:
current_info:
runs-on: ubuntu-latest
name: Current Information
outputs:
version: ${{ steps.release_info.outputs.version }}
tag: ${{ steps.release_info.outputs.tag }}
steps:
- name: Checkout master
uses: actions/checkout@master
with:
ref: "${{ github.ref }}"
fetch-depth: 0 # This is set to download the full git history for the repo
- name: Get Current Version
id: release_info
uses: revam/gh-action-get-tag-and-version@v1
with:
branch: false
prefix: "v"
prefixRegex: "[vV]?"
suffixRegex: "dev"
suffix: "dev"
build_plugin:
runs-on: ubuntu-latest
needs:
- current_info
name: Build Release
steps:
- name: Checkout
uses: actions/checkout@master
with:
ref: ${{ github.ref }}
fetch-depth: 0 # This is set to download the full git history for the repo
- name: Fetch Stable Manifest from Metadata Branch
run: |
git checkout origin/metadata -- stable/manifest.json;
git reset;
rm manifest.json;
mv stable/manifest.json manifest.json;
rmdir stable;
- name: Setup .Net
uses: actions/setup-dotnet@v1
with:
dotnet-version: 9.0.x
- name: Restore Nuget Packages
run: dotnet restore Shokofin/Shokofin.csproj
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install JPRM
run: python -m pip install jprm
- name: Run JPRM
run: python build_plugin.py --repo ${{ github.repository }} --version=${{ needs.current_info.outputs.version }} --tag=${{ needs.current_info.outputs.tag }}
- name: Change to Metadata Branch
run: |
mkdir stable;
mv manifest.json stable
git add ./stable/manifest.json;
git stash push --staged --message "Temp release details";
git reset --hard;
git checkout origin/metadata -B metadata;
git stash apply || git checkout --theirs stable/manifest.json;
git reset;
- name: Update Release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/shoko_*.zip
tag: ${{ github.ref }}
file_glob: true
- name: Update Stable Manifest
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: metadata
commit_message: "misc: update stable manifest"
file_pattern: stable/manifest.json
skip_fetch: true
================================================
FILE: .github/workflows/release_draft.jq
================================================
group_by(.simple_type) |
sort_by(.[0].simple_type | ({ "feat": 0, "change": 1, "fix": 2, "repo": 3}[.] // 99)) |
reduce .[] as $group (
"";
reduce $group[] as $commit (
. + "## `\($group.[0].simple_type)`\n\n";
. +
"- \($commit.subject). (\($commit.commit)) by @\($commit.author.github) (`index: \($commit.index)`)" +
if $commit.isSkipCI then " (_Skip CI_)" else "" end +
if $commit.body != null and $commit.body != "" then
"\n\n \($commit.body | gsub("\n"; "\n "))"
else
""
end +
"\n\n"
)
)
================================================
FILE: .gitignore
================================================
# Common IntelliJ Platform excludes
# User specific
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/httpRequests/
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
*.suo
*.user
.vs/
[Bb]in/
[Oo]bj/
_UpgradeReport_Files/
[Pp]ackages/
Thumbs.db
Desktop.ini
.DS_Store
/.idea/
/.venv
artifacts
.github/workflows/email-to-github.json
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit",
"eamodio.gitlens",
"streetsidesoftware.code-spell-checker"
],
"unwantedRecommendations": []
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.tabSize": 4,
"files.trimTrailingWhitespace": false,
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false,
"dotnet.defaultSolution": "Shokofin.sln",
"cSpell.words": [
"anidb",
"apikey",
"automagic",
"automagically",
"boxset",
"dlna",
"ecchi",
"emby",
"eroge",
"fanart",
"fanarts",
"Gainax",
"hentai",
"imdb",
"imdbid",
"interrobang",
"jellyfin",
"josei",
"jprm",
"kodomo",
"koma",
"linkbutton",
"manhua",
"manhwa",
"mina",
"nfo",
"nfos",
"outro",
"registrator",
"scrobble",
"scrobbled",
"scrobbling",
"seinen",
"seiyuu",
"serilog",
"shoko",
"shokofin",
"shoujo",
"shounen",
"signalr",
"tmdb",
"trickplay",
"tvshow",
"tvshows",
"viewshow",
"webui",
"whitespaces"
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Shoko - Anime Cataloging Program
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Shokofin
A Jellyfin plugin to integrate [Jellyfin](https://jellyfin.org/) with
[Shoko Server](https://shokoanime.com/downloads/shoko-server).
## Read this before installing
**This plugin requires that you have already set up and are using Shoko Server**,
and that the files you intend to include in Jellyfin are **indexed** (and
optionally managed) by Shoko Server. **Otherwise, the plugin won't be able to
provide metadata for your files**, since there is no metadata to provide for them.
### What Is Shoko?
Shoko is an anime cataloging program designed to automate the cataloging of your
collection regardless of the size and amount of files you have. Unlike other
anime cataloging programs which make you manually add your series or link the
files to them, Shoko removes the tedious, time-consuming and boring task of
having to manually add every file and manually input the file information. You
have better things to do with your time like actually watching the series in
your collection so let Shoko handle all the heavy lifting.
Learn more about Shoko at https://shokoanime.com/.
## Documentation
Head over to our [documentation site](https://docs.shokoanime.com/jellyfin/installing-shokofin) for documentation that is not pure source-code.
## Install
There are multiple ways to install the plugin, but the recommended way is to use
the official Jellyfin repository.
Below is a version compatibility matrix for which version of Shokofin is
compatible with what.
| Shokofin | Jellyfin | Shoko Server |
|-------------------|-------------------|-------------------|
| `0.x.x` | `10.7` | `4.0.0` — `4.1.2` |
| `1.x.x` | `10.7` | `4.1.0` — `4.1.2` |
| `2.x.x` | `10.8` | `4.1.2` |
| `3.x.x` | `10.8` | `4.2.0` |
| `4.0.0` — `4.1.1` | `10.9` | `4.2.2` |
| `4.2.0` — `4.2.2` | `10.9` | `4.2.2` — `5.0.0` |
| `5.0.0` | `10.10` | `5.0.0` |
| `5.0.1` — `5.0.4` | `10.10` | `5.0.0` — `5.1.0` |
| `5.0.5` — `5.0.6` | `10.11` | `5.1.0` |
| `6.0.0` | `10.11` | `5.2.0` — `5.2.5` |
| `6.0.1` — `6.0.3` | `10.10` — `10.11` | `5.2.0` — `5.3.0` |
| `6.0.4` — `6.0.5` | `10.10` — `10.11` | `5.2.0` — `5.3.2` |
| `dev` | `10.10` — `10.11` | `dev` |
### Official Repository
#### Jellyfin 10.11
1. **Access Plugin Repositories:**
- Go to `Dashboard` -> `Plugins` -> `Manage Repositories` -> `New Repository`.
2. **Add New Repository:**
- Add a new repository with the following details:
* **Repository Name:** `Shokofin Stable`
* **Repository URL:** `https://raw.githubusercontent.com/ShokoAnime/Shokofin/metadata/stable/manifest.json`
3. **Install Shokofin:**
- Go back to the catalog in the plugins section of the dashboard, filter to `All` or `Available` plugins,
then refresh the browser page to reload the plugin list.
- Find and install `Shoko` from the list, optionally by filtering the list by the `Anime` category.
4. **Restart Jellyfin:**
- Restart your server to apply the changes.
#### Jellyfin 10.10
1. **Access Plugin Repositories:**
- Go to `Dashboard` -> `Plugins` -> `Catalog` -> `⚙ Gear icon`.
2. **Add New Repository:**
- Add a new repository with the following details:
* **Repository Name:** `Shokofin Stable`
* **Repository URL:** `https://raw.githubusercontent.com/ShokoAnime/Shokofin/metadata/stable/manifest.json`
3. **Install Shokofin:**
- Go to the catalog in the plugins section of the dashboard.
- Find and install `Shoko` from the `Anime` section.
### Github Releases
1. **Download the Plugin:**
- Go to the latest release on GitHub [here](https://github.com/ShokoAnime/shokofin/releases/latest).
- Download the `shoko_*.zip` file.
2. **Extract and Place Files:**
- Extract all `.dll` files and `meta.json` from the zip file.
- Put them in a folder named `Shoko`.
- Copy this `Shoko` folder to the `plugins` folder in your Jellyfin program
data directory or inside the Jellyfin install directory. For help finding
your Jellyfin install location, check the "Data Directory" section on
[this page](https://jellyfin.org/docs/general/administration/configuration.html).
3. **Restart Jellyfin:**
- Start or restart your Jellyfin server to apply the changes.
### Build Process
1. **Clone or Download the Repository:**
- Clone or download the repository from GitHub.
2. **Set Up .NET Core SDK:**
- Make sure you have the .NET Core SDK installed on your computer.
3. **Build the Plugin:**
- Open a terminal and navigate to the repository directory.
- Run the following commands to restore and publish the project:
```sh
$ dotnet restore Shokofin/Shokofin.csproj
$ dotnet publish -c Release Shokofin/Shokofin.csproj
```
4. **Copy Built Files:**
- After building, go to the `bin/Release/net9.0/` directory.
- Copy all `.dll` files to a folder named `Shoko`.
- Place this `Shoko` folder in the `plugins` directory of your Jellyfin
program data directory or inside the portable install directory. For help
finding your Jellyfin install location, check the "Data Directory" section
on [this page](https://jellyfin.org/docs/general/administration/configuration.html).
## Feature Overview
- [ ] Metadata integration
- [X] Basic metadata, e.g. titles, description, dates, etc.
- [X] Customizable main title for items
- [X] Optional customizable alternate/original title for items
- [X] Customizable description source for items
Choose between AniDB, TvDB, TMDB, or a mix of the three.
- [X] Support optionally adding titles and descriptions for all episodes for
multi-entry files.
- [X] Genres
With settings to choose which tags to add as genres.
- [X] Tags
With settings to choose which tags to add as tags.
- [X] Official Ratings
Currently only _assumed_ ratings using AniDB tags or manual overrides using custom user tags are available. Also with settings to choose which providers to use.
- [X] Production Locations
With settings to chose which provider to use.
- [ ] Staff
- [X] Displayed on the Show/Season/Movie items
- [X] Images
- [ ] Metadata Provider
_Needs to add endpoints to the Shoko Server side first._
- [ ] Studios
- [X] Displayed on the Show/Season/Movie items
- [ ] Images
_Needs to add support and endpoints to the Shoko Server side **or** fake it client-side first._
- [ ] Metadata Provider
_Needs to add support and endpoints to the Shoko Server side **or** fake it client-side first._
- [X] Library integration
- [X] Support for different library types
- [X] Show library
- [X] Movie library
- [X] Mixed show/movie library.
_As long as the VFS is in use for the media library. Also keep in mind that this library type is poorly supported in Jellyfin Core, and we can't work around the poor internal support, so you'll have to take what you get or leave it as is._
- [X] Supports adding local trailers
- [X] on Show items
- [X] on Season items
- [X] on Movie items
- [X] Specials and extra features.
- [X] Customize how Specials are placed in your library. I.e. if they are
mapped to the normal seasons, or if they are strictly kept in season zero.
- [X] Extra features. The plugin will map specials stored in Shoko such as
interviews, etc. as extra features, and all other specials as episodes in
season zero.
- [X] Map OPs/EDs to Theme Videos, so they can be displayed as background video
while you browse your library.
- [X] Support merging multi-version episodes/movies into a single entry.
Tidying up the UI if you have multiple versions of the same episode or
movie.
- [X] Auto merge after library scan (if enabled).
- [X] Manual merge/split tasks
- [X] Support optionally setting other provider IDs Shoko knows about on some item types when an ID is available for the items in Shoko.
_Only AniDB and TMDB IDs are available for now._
- [X] Multiple ways to organize your library.
- [X] Choose between two ways to group your Shows/Seasons; using AniDB Anime structure (the default mode), or using Shoko Groups.
_For the best compatibility if you're not using the VFS it is **strongly** advised **not** to use "season" folders with anime as it limits which grouping you can use, you can still create "seasons" in the UI using Shoko's groups._
- [X] Optionally create Collections for…
- [X] Movies using the Shoko series.
- [X] Movies and Shows using the Shoko groups.
- [X] Supports separating your on-disc library into a two Show and Movie
libraries.
_Provided you apply the workaround to support it_.
- [X] Automatically populates all missing episodes not in your collection, so
you can see at a glance what you are missing out on.
- [X] Optionally react to events sent from Shoko.
- [X] User data
- [X] Able to sync the watch data to/from Shoko on a per-user basis in
multiple ways. And Shoko can further sync the to/from other linked services.
- [X] During import.
- [X] Player events (play/pause/resume/stop events)
- [X] After playback (stop event)
- [X] Live scrobbling (every 1 minute during playback after the last
play/resume event or when jumping)
- [X] Import and export user data tasks
- [X] Virtual File System (VFS)
_Allows us to disregard the underlying disk file structure while automagically meeting Jellyfin's requirements for file organization._
================================================
FILE: Shokofin/API/Converters/JsonAutoStringConverter.cs
================================================
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Shokofin.API.Converters;
/// <summary>
/// Automatically converts JSON values to a string.
/// </summary>
public class JsonAutoStringConverter : JsonConverter<string> {
public override bool CanConvert(Type typeToConvert)
=> typeof(string) == typeToConvert;
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
if (reader.TokenType is JsonTokenType.Number) {
if (reader.TryGetInt64(out var number))
return number.ToString(CultureInfo.InvariantCulture);
if (reader.TryGetDouble(out var doubleNumber))
return doubleNumber.ToString(CultureInfo.InvariantCulture);
}
if (reader.TokenType is JsonTokenType.String)
return reader.GetString();
using var document = JsonDocument.ParseValue(ref reader);
return document.RootElement.Clone().ToString();
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
=> writer.WriteStringValue(value);
}
================================================
FILE: Shokofin/API/IdPrefix.cs
================================================
namespace Shokofin.API;
internal struct IdPrefix {
internal const char TmdbShow = 't';
internal const char TmdbMovie = 'T';
internal const char TmdbMovieCollection = 'Ʇ';
}
================================================
FILE: Shokofin/API/Info/AniDB/AnidbAnimeInfo.cs
================================================
namespace Shokofin.API.Info.AniDB;
public class AnidbAnimeInfo {
public required string AnidbAnimeId { get; init; }
}
================================================
FILE: Shokofin/API/Info/AniDB/AnidbEpisodeInfo.cs
================================================
using Shokofin.API.Models;
using Shokofin.Extensions;
namespace Shokofin.API.Info.AniDB;
public class AnidbEpisodeInfo {
public required string AnidbEpisodeId { get; init; }
public required string AnidbAnimeId { get; init; }
public required int EpisodeNumber { get; init; }
public required EpisodeType Type { get; init; }
public string GetEpisodeNumberText() => Type.ToShortString() + EpisodeNumber;
}
================================================
FILE: Shokofin/API/Info/CollectionInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Shokofin.API.Models;
using Shokofin.API.Models.Shoko;
using Shokofin.Utils;
namespace Shokofin.API.Info;
public class CollectionInfo(ShokoGroup group, string? mainSeasonId, List<ShowInfo> shows, List<CollectionInfo> subCollections) : IBaseItemInfo {
/// <summary>
/// Collection Identifier.
/// </summary>
public string Id { get; init; } = group.Id;
/// <summary>
/// Parent Collection Identifier, if any.
/// </summary>
public string? ParentId { get; init; } = group.IDs.ParentGroup?.ToString();
/// <summary>
/// Top Level Collection Identifier. Will refer to itself if it's a top level collection.
/// </summary>
public string TopLevelId { get; init; } = group.IDs.TopLevelGroup.ToString();
/// <summary>
/// Main show's main season identifier.
/// </summary>
public string? MainSeasonId { get; init; } = mainSeasonId;
/// <summary>
/// True if the collection is a top level collection.
/// </summary>
public bool IsTopLevel { get; init; } = group.IDs.TopLevelGroup == group.IDs.Shoko;
/// <summary>
/// Collection Name.
/// </summary>
public string Title { get; init; } = group.Name;
public IReadOnlyList<Title> Titles { get; init; } = [];
/// <summary>
/// Collection Description.
/// </summary>
public string Overview { get; init; } = group.Description;
public IReadOnlyList<Text> Overviews { get; init; } = [];
public IReadOnlyList<string> Notes { get; init; } = [];
public string? OriginalLanguageCode => null;
public DateTime CreatedAt { get; init; }
public DateTime LastUpdatedAt { get; init; }
/// <summary>
/// Number of files across all shows and movies in the collection and all sub-collections.
/// </summary>
public int FileCount { get; init; } = group.Sizes.Files;
/// <summary>
/// Shows in the collection and not in any sub-collections.
/// </summary>
public IReadOnlyList<ShowInfo> Shows { get; init; } = shows
.Where(showInfo => !showInfo.IsMovieCollection)
.ToList();
/// <summary>
/// Movies in the collection and not in any sub-collections.
/// </summary>
public IReadOnlyList<ShowInfo> Movies { get; init; } = shows
.Where(showInfo => showInfo.IsMovieCollection)
.ToList();
/// <summary>
/// Sub-collections of the collection.
/// </summary>
public IReadOnlyList<CollectionInfo> SubCollections { get; init; } = subCollections;
public CollectionInfo(ShokoGroup group, ShokoSeries series, string? mainSeasonId, List<ShowInfo> shows, List<CollectionInfo> subCollections) : this(group, mainSeasonId, shows, subCollections)
{
Title = series.Name;
Titles = series.AniDB.Titles;
Overview = series.Description == series.AniDB.Description
? TextUtility.SanitizeAnidbDescription(series.Description)
: series.Description;
Overviews = [
new() {
IsDefault = true,
IsPreferred = true,
LanguageCode = "en",
Source = "AniDB",
Value = TextUtility.SanitizeAnidbDescription(series.AniDB.Description, out var notes),
},
];
Notes = notes;
CreatedAt = group.CreatedAt;
LastUpdatedAt = group.LastUpdatedAt;
}
}
================================================
FILE: Shokofin/API/Info/EpisodeInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using Shokofin.API.Info.AniDB;
using Shokofin.API.Info.Shoko;
using Shokofin.API.Info.TMDB;
using Shokofin.API.Models;
using Shokofin.API.Models.Shoko;
using Shokofin.API.Models.TMDB;
using Shokofin.Events.Interfaces;
using Shokofin.Extensions;
using Shokofin.ExternalIds;
using Shokofin.Utils;
namespace Shokofin.API.Info;
public class EpisodeInfo : IExtendedItemInfo {
private static readonly HashSet<string> _invalidPhrases = [
"seisaku iinkai", // Production Committee
];
private readonly ShokoApiClient _client;
public string Id { get; init; }
public string InternalId => ShokoInternalId.EpisodeNamespace + Id;
public string SeasonId { get; init; }
public EpisodeType Type { get; init; }
public bool IsHidden { get; init; }
public bool IsMainEntry { get; init; }
public bool IsStandalone { get; init; }
public int? SeasonNumber { get; init; }
public int EpisodeNumber { get; init; }
public string Title { get; init; }
public IReadOnlyList<Title> Titles { get; init; }
public string? Overview { get; init; }
public IReadOnlyList<Text> Overviews { get; init; }
public IReadOnlyList<string> Notes { get; init; } = [];
public string? OriginalLanguageCode { get; init; }
public ExtraType? ExtraType { get; init; }
public TimeSpan? Runtime { get; init; }
public DateTime? AiredAt { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime LastUpdatedAt { get; init; }
public Rating CommunityRating { get; init; }
public IReadOnlyList<string> Genres { get; init; }
public IReadOnlyList<string> Tags { get; init; }
public IReadOnlyList<string> Studios { get; init; }
public IReadOnlyDictionary<ProviderName, IReadOnlyList<string>> ProductionLocations { get; init; }
public IReadOnlyList<Models.ContentRating> ContentRatings { get; init; }
public IReadOnlyList<PersonInfo> Staff { get; init; }
public List<CrossReference.EpisodeCrossReferenceIDs> CrossReferences { get; init; }
public bool IsAvailable => CrossReferences.Count is > 0;
#region Shoko Episode Metadata
public ShokoEpisodeInfo[] ShokoEpisodes { get; init; }
#endregion
#region Anidb Episode Metadata
public string? AnidbEpisodeId => AnidbEpisodes.FirstOrDefault()?.AnidbEpisodeId;
public AnidbEpisodeInfo[] AnidbEpisodes { get; init; }
#endregion
#region TMDB Movie Metadata
public string? TmdbMovieId => TmdbMovies.FirstOrDefault()?.TmdbMovieId;
public TmdbMovieInfo[] TmdbMovies { get; init; }
#endregion
#region TMDB Episode Metadata
public string? TmdbEpisodeId => TmdbEpisodes.FirstOrDefault()?.TmdbEpisodeId;
public string? TvdbEpisodeId => TmdbEpisodes.FirstOrDefault()?.TvdbEpisodeId;
public TmdbEpisodeInfo[] TmdbEpisodes { get; init; }
#endregion
public EpisodeInfo(
ShokoApiClient client,
ShokoEpisode episode,
IReadOnlyList<Role> cast,
List<string> genres,
List<string> tags,
string[] productionLocations,
string? anidbContentRating,
TmdbMovieInfo[] tmdbMovies,
TmdbEpisodeInfo[] tmdbEpisodes,
ITmdbEntity? tmdbEntity = null,
ITmdbParentEntity? tmdbParentEntity = null
) {
var contentRatings = new List<Models.ContentRating>();
var productionLocationDict = new Dictionary<ProviderName, IReadOnlyList<string>>();
var tmdbMovie = tmdbEntity as TmdbMovie;
var tmdbEpisode = tmdbEntity as TmdbEpisode;
var isMainEntry = episode.AniDB.Titles.FirstOrDefault(t => t.LanguageCode is "en")?.Value is { } mainAnidbTitle &&
TextUtility.IgnoredSubTitles.Contains(mainAnidbTitle);
if (!string.IsNullOrEmpty(anidbContentRating))
contentRatings.Add(new() {
Rating = anidbContentRating,
Country = "US",
Language = "en",
Source = "AniDB",
});
if (productionLocations.Length > 0)
productionLocationDict[ProviderName.AniDB] = productionLocations;
_client = client;
Id = episode.Id;
SeasonId = episode.IDs.ParentSeries.ToString();
Type = episode.AniDB.Type;
IsHidden = episode.IsHidden;
IsMainEntry = isMainEntry;
IsStandalone = tmdbMovie is not null || isMainEntry;
SeasonNumber = null;
EpisodeNumber = episode.AniDB.EpisodeNumber;
ExtraType = Ordering.GetExtraType(episode.AniDB);
Title = episode.Name;
Titles = [
..episode.AniDB.Titles,
..(tmdbEntity?.Titles ?? []),
];
var notes = (IReadOnlyList<string>)[];
Overview = episode.Description == episode.AniDB.Description
? TextUtility.SanitizeAnidbDescription(episode.Description)
: episode.Description;
Overviews = [
..(!string.IsNullOrEmpty(episode.AniDB.Description) ? [
new() {
IsDefault = true,
IsPreferred = string.Equals(episode.Description, episode.AniDB.Description),
LanguageCode = "en",
Source = "AniDB",
Value = TextUtility.SanitizeAnidbDescription(episode.AniDB.Description, out notes),
},
] : Array.Empty<Text>()),
..(tmdbEntity?.Overviews ?? []),
];
Notes = notes;
Studios = [];
if (tmdbMovie is not null) {
Runtime = tmdbMovie.Runtime ?? episode.AniDB.Duration;
AiredAt = tmdbMovie.ReleasedAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
CommunityRating = tmdbMovie.UserRating;
Staff = tmdbMovie.Cast.Concat(tmdbMovie.Crew)
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), MetadataProvider.Tmdb.ToString()))
.WhereNotNull()
.ToArray();
if (Staff.Count is 0)
Staff = cast
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), ProviderNames.Anidb))
.WhereNotNull()
.ToArray();
productionLocationDict[ProviderName.TMDB] = tmdbMovie.ProductionCountries.Values.ToArray();
contentRatings.AddRange(tmdbMovie.ContentRatings);
Studios = tmdbMovie.Studios.Select(r => r.Name).Distinct().Order().ToArray();
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
tags.AddRange(tmdbMovie.Keywords);
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbGenres))
tags.AddRange(tmdbMovie.Genres);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
genres.AddRange(tmdbMovie.Keywords);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbGenres))
genres.AddRange(tmdbMovie.Genres);
}
else if (tmdbEpisode is not null) {
Runtime = tmdbEpisode.Runtime ?? episode.AniDB.Duration;
AiredAt = tmdbEpisode.AiredAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
CommunityRating = tmdbEpisode.UserRating;
Staff = tmdbEpisode.Cast.Concat(tmdbEpisode.Crew)
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), MetadataProvider.Tmdb.ToString()))
.WhereNotNull()
.ToArray();
if (Staff.Count is 0)
Staff = cast
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), ProviderNames.Anidb))
.WhereNotNull()
.ToArray();
if (tmdbParentEntity is not null) {
productionLocationDict[ProviderName.TMDB] = tmdbParentEntity.ProductionCountries.Values.ToArray();
contentRatings.AddRange(tmdbParentEntity.ContentRatings);
Studios = tmdbParentEntity.Studios.Select(r => r.Name).Distinct().Order().ToArray();
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
tags.AddRange(tmdbParentEntity.Keywords);
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbGenres))
tags.AddRange(tmdbParentEntity.Genres);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
genres.AddRange(tmdbParentEntity.Keywords);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbGenres))
genres.AddRange(tmdbParentEntity.Genres);
}
}
else {
var onlyAnimationWorks = Plugin.Instance.Configuration.Metadata_StudioOnlyAnimationWorks;
Runtime = episode.AniDB.Duration;
AiredAt = episode.AniDB.AirDate;
CommunityRating = episode.AniDB.Rating;
Staff = cast
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), ProviderNames.Anidb))
.WhereNotNull()
.ToArray();
Studios = cast
.Where(role =>
!string.IsNullOrEmpty(role.Staff.Name) &&
role.Type is CreatorRoleType.Studio &&
role.Staff.Type is null or "Company" &&
!_invalidPhrases.Any(p => role.Staff.Name.Contains(p, StringComparison.OrdinalIgnoreCase)) &&
(!onlyAnimationWorks || role.Name is "Animation Works")
)
.Select(r => r.Staff.Name)
.Distinct()
.Order()
.ToArray();
}
CreatedAt = episode.CreatedAt;
LastUpdatedAt = episode.LastUpdatedAt;
Genres = genres.Distinct().Order().ToArray();
Tags = tags.Distinct().Order().ToArray();
ProductionLocations = productionLocationDict;
ContentRatings = contentRatings.Distinct().ToList();
CrossReferences = episode.CrossReferences;
ShokoEpisodes = [episode.ToInfo()];
AnidbEpisodes = [episode.AniDB.ToInfo()];
TmdbMovies = tmdbMovies;
TmdbEpisodes = tmdbEpisodes;
}
public EpisodeInfo(ShokoApiClient client, TmdbEpisode tmdbEpisode, TmdbShow tmdbShow, ShokoEpisodeInfo[] shokoEpisodes, AnidbEpisodeInfo[] anidbEpisodes) {
var tags = new List<string>();
var genres = new List<string>();
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
tags.AddRange(tmdbShow.Keywords);
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbGenres))
tags.AddRange(tmdbShow.Genres);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
genres.AddRange(tmdbShow.Keywords);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbGenres))
genres.AddRange(tmdbShow.Genres);
_client = client;
Id = IdPrefix.TmdbShow + tmdbEpisode.Id.ToString();
SeasonId = IdPrefix.TmdbShow + tmdbEpisode.SeasonId;
Type = tmdbEpisode.SeasonNumber is 0 ? EpisodeType.Special : EpisodeType.Episode;
IsHidden = false;
IsMainEntry = false;
IsStandalone = false;
SeasonNumber = tmdbEpisode.SeasonNumber;
EpisodeNumber = tmdbEpisode.EpisodeNumber;
Title = tmdbEpisode.Title;
Titles = tmdbEpisode.Titles;
Overview = tmdbEpisode.Overview;
Overviews = tmdbEpisode.Overviews;
OriginalLanguageCode = tmdbShow.OriginalLanguage;
ExtraType = null;
Runtime = tmdbEpisode.Runtime;
AiredAt = tmdbEpisode.AiredAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
CreatedAt = tmdbEpisode.CreatedAt;
LastUpdatedAt = tmdbEpisode.LastUpdatedAt;
CommunityRating = tmdbEpisode.UserRating;
Genres = genres.Distinct().Order().ToList();
Tags = tags.Distinct().Order().ToList();
Studios = tmdbShow.Studios.Select(r => r.Name).Distinct().Order().ToArray();
ProductionLocations = new Dictionary<ProviderName, IReadOnlyList<string>>() {
{ ProviderName.TMDB, tmdbShow.ProductionCountries.Values.ToArray() },
};
ContentRatings = tmdbShow.ContentRatings;
Staff = tmdbEpisode.Cast.Concat(tmdbEpisode.Crew)
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), MetadataProvider.Tmdb.ToString()))
.WhereNotNull()
.ToArray();
CrossReferences = tmdbEpisode.FileCrossReferences
.SelectMany(a => a.Episodes)
.DistinctBy(a => (a.ED2K, a.FileSize))
.ToList();
ShokoEpisodes = shokoEpisodes;
AnidbEpisodes = anidbEpisodes;
TmdbEpisodes = [tmdbEpisode.ToInfo()];
TmdbMovies = [];
}
public EpisodeInfo(ShokoApiClient client, TmdbMovie tmdbMovie, ShokoEpisodeInfo[] shokoEpisodes, AnidbEpisodeInfo[] anidbEpisodes) {
var tags = new List<string>();
var genres = new List<string>();
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
tags.AddRange(tmdbMovie.Keywords);
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbGenres))
tags.AddRange(tmdbMovie.Genres);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
genres.AddRange(tmdbMovie.Keywords);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbGenres))
genres.AddRange(tmdbMovie.Genres);
_client = client;
Id = IdPrefix.TmdbMovie + tmdbMovie.Id.ToString();
SeasonId = tmdbMovie.CollectionId.HasValue && Plugin.Instance.Configuration.SeparateMovies && Plugin.Instance.Configuration.CollectionGrouping is Ordering.CollectionCreationType.Movies
? IdPrefix.TmdbMovieCollection + tmdbMovie.CollectionId.Value.ToString()
: IdPrefix.TmdbMovie + tmdbMovie.Id.ToString();
Type = EpisodeType.Episode;
IsHidden = false;
IsMainEntry = false;
IsStandalone = true;
SeasonNumber = null;
EpisodeNumber = 1;
Title = tmdbMovie.Title;
Titles = tmdbMovie.Titles;
Overview = tmdbMovie.Overview;
Overviews = tmdbMovie.Overviews;
OriginalLanguageCode = tmdbMovie.OriginalLanguage;
ExtraType = null;
Runtime = tmdbMovie.Runtime;
AiredAt = tmdbMovie.ReleasedAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
CreatedAt = tmdbMovie.CreatedAt;
LastUpdatedAt = tmdbMovie.LastUpdatedAt;
CommunityRating = tmdbMovie.UserRating;
Genres = genres.Distinct().Order().ToList();
Tags = tags.Distinct().Order().ToList();
Studios = tmdbMovie.Studios.Select(r => r.Name).Distinct().Order().ToArray();
ProductionLocations = new Dictionary<ProviderName, IReadOnlyList<string>>() {
{ ProviderName.TMDB, tmdbMovie.ProductionCountries.Values.ToArray() },
};
ContentRatings = tmdbMovie.ContentRatings;
Staff = tmdbMovie.Cast.Concat(tmdbMovie.Crew)
.GroupBy(role => (role.Type, role.Staff.Id))
.Select(roles => RoleToPersonInfo(roles.ToList(), MetadataProvider.Tmdb.ToString()))
.WhereNotNull()
.ToArray();
CrossReferences = tmdbMovie.FileCrossReferences
.SelectMany(a => a.Episodes)
.DistinctBy(a => (a.ED2K, a.FileSize))
.ToList();
ShokoEpisodes = shokoEpisodes;
AnidbEpisodes = anidbEpisodes;
TmdbEpisodes = [];
TmdbMovies = [tmdbMovie.ToInfo()];
}
public async Task<EpisodeImages> GetImages(CancellationToken cancellationToken)
=> Id[0] switch {
IdPrefix.TmdbShow => await _client.GetImagesForTmdbEpisode(Id[1..], cancellationToken).ConfigureAwait(false),
IdPrefix.TmdbMovie => await _client.GetImagesForTmdbMovie(Id[1..], cancellationToken).ConfigureAwait(false),
_ => await _client.GetImagesForShokoEpisode(Id, cancellationToken).ConfigureAwait(false),
} ?? new();
private static string? GetImagePath(Image image)
=> image != null && image.IsAvailable ? image.ToURLString(internalUrl: true) : null;
private static PersonInfo? RoleToPersonInfo(IReadOnlyList<Role> roles, string roleProvider)
=> string.IsNullOrWhiteSpace(roles[0].Staff.Name) ? null : roles[0].Type switch {
CreatorRoleType.Director => new PersonInfo {
Type = PersonKind.Director,
Name = roles[0].Staff.Name,
Role = roles[0].Name,
ImageUrl = GetImagePath(roles[0].Staff.Image),
ProviderIds = new() {
{ roleProvider, roles[0].Staff.Id!.Value.ToString() },
},
},
CreatorRoleType.Producer => new PersonInfo {
Type = PersonKind.Producer,
Name = roles[0].Staff.Name,
Role = roles[0].Name,
ImageUrl = GetImagePath(roles[0].Staff.Image),
ProviderIds = new() {
{ roleProvider, roles[0].Staff.Id!.Value.ToString() },
},
},
CreatorRoleType.Music => new PersonInfo {
Type = PersonKind.Lyricist,
Name = roles[0].Staff.Name,
Role = roles[0].Name,
ImageUrl = GetImagePath(roles[0].Staff.Image),
ProviderIds = new() {
{ roleProvider, roles[0].Staff.Id!.Value.ToString() },
},
},
CreatorRoleType.SourceWork => new PersonInfo {
Type = PersonKind.Writer,
Name = roles[0].Staff.Name,
Role = roles[0].Name,
ImageUrl = GetImagePath(roles[0].Staff.Image),
ProviderIds = new() {
{ roleProvider, roles[0].Staff.Id!.Value.ToString() },
},
},
CreatorRoleType.SeriesComposer => new PersonInfo {
Type = PersonKind.Composer,
Name = roles[0].Staff.Name,
ImageUrl = GetImagePath(roles[0].Staff.Image),
ProviderIds = new() {
{ roleProvider, roles[0].Staff.Id!.Value.ToString() },
},
},
CreatorRoleType.Actor => new PersonInfo {
Type = PersonKind.Actor,
Name = roles[0].Staff.Name,
Role = roles.Select(role => role?.Character?.Name ?? string.Empty).Where(role => !string.IsNullOrEmpty(role)).Distinct().Order().Join(" / "),
ImageUrl = GetImagePath(roles[0].Staff.Image),
ProviderIds = new() {
{ roleProvider, roles[0].Staff.Id!.Value.ToString() },
},
},
_ => null,
};
}
================================================
FILE: Shokofin/API/Info/FileInfo.cs
================================================
using System.Collections.Generic;
using System.Linq;
using Shokofin.API.Models;
using Shokofin.ExternalIds;
namespace Shokofin.API.Info;
public class FileInfo(File file, string seriesId, IReadOnlyList<(EpisodeInfo Episode, CrossReference.EpisodeCrossReferenceIDs CrossReference, string Id)> episodeList) {
public string Id { get; init; } = file.Id.ToString();
private string? _internalId;
public string InternalId => _internalId ??= ShokoInternalId.FileNamespace + Id + $"?seriesId={SeriesId}&episodeIds={string.Join(",", EpisodeList.Select(tuple => tuple.Id))}&seasonId={EpisodeList.FirstOrDefault(tuple => tuple.Episode.SeasonId != null).Episode?.SeasonId ?? string.Empty}";
public string SeriesId { get; init; } = seriesId;
public MediaBrowser.Model.Entities.ExtraType? ExtraType { get; init; } = episodeList.FirstOrDefault(tuple => tuple.Episode.ExtraType != null).Episode?.ExtraType;
public File Shoko { get; init; } = file;
public IReadOnlyList<(EpisodeInfo Episode, CrossReference.EpisodeCrossReferenceIDs CrossReference, string Id)> EpisodeList { get; init; } = episodeList;
}
================================================
FILE: Shokofin/API/Info/IBaseItemInfo.cs
================================================
using System;
using System.Collections.Generic;
using Shokofin.API.Models;
namespace Shokofin.API.Info;
/// <summary>
/// Information about a base item.
/// </summary>
public interface IBaseItemInfo {
/// <summary>
/// Unique identifier for the base item.
/// </summary>
string Id { get; }
/// <summary>
/// Preferred title according to title settings on the server for the base item type.
/// </summary>
string Title { get; }
/// <summary>
/// List of all available titles for the base item.
/// </summary>
IReadOnlyList<Title> Titles { get; }
/// <summary>
/// Preferred overview according to description settings on the server.
/// </summary>
string? Overview { get; }
/// <summary>
/// List of all available overviews for the base item.
/// </summary>
IReadOnlyList<Text> Overviews { get; }
/// <summary>
/// Notes.
/// </summary>
IReadOnlyList<string> Notes { get => []; }
/// <summary>
/// Original language code for the base item if available.
/// </summary>
string? OriginalLanguageCode { get; }
/// <summary>
/// Date and time the base item was created.
/// </summary>
DateTime CreatedAt { get; }
/// <summary>
/// Date and time the base item was last updated.
/// </summary>
DateTime LastUpdatedAt { get; }
}
================================================
FILE: Shokofin/API/Info/IExtendedItemInfo.cs
================================================
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using Shokofin.API.Models;
using Shokofin.Events.Interfaces;
namespace Shokofin.API.Info;
public interface IExtendedItemInfo : IBaseItemInfo {
bool IsAvailable { get; }
IReadOnlyList<string> Tags { get; }
IReadOnlyList<string> Genres { get; }
IReadOnlyList<string> Studios { get; }
IReadOnlyDictionary<ProviderName, IReadOnlyList<string>> ProductionLocations { get; }
IReadOnlyList<ContentRating> ContentRatings { get; }
IReadOnlyList<PersonInfo> Staff { get; }
}
================================================
FILE: Shokofin/API/Info/SeasonInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Shokofin.API.Info.AniDB;
using Shokofin.API.Info.Shoko;
using Shokofin.API.Info.TMDB;
using Shokofin.API.Models;
using Shokofin.API.Models.Shoko;
using Shokofin.API.Models.TMDB;
using Shokofin.Configuration;
using Shokofin.Events.Interfaces;
using Shokofin.Extensions;
using Shokofin.ExternalIds;
using Shokofin.Utils;
using ContentRating = Shokofin.API.Models.ContentRating;
using PersonInfo = MediaBrowser.Controller.Entities.PersonInfo;
namespace Shokofin.API.Info;
public class SeasonInfo : IExtendedItemInfo {
private readonly ShokoApiClient _client;
public string Id { get; init; }
public string InternalId => ShokoInternalId.SeriesNamespace + Id;
public IReadOnlyList<string> ExtraIds { get; init; }
public string? TopLevelShokoGroupId { get; init; }
public SeriesType Type { get; init; }
public SeriesStructureType StructureType { get; init; }
public Ordering.OrderType SeasonOrdering { get; init; }
public Ordering.SpecialOrderType SpecialsPlacement { get; init; }
public bool IsMultiEntry { get; init; }
public bool IsRestricted { get; init; }
public string Title { get; init; }
public IReadOnlyList<Title> Titles { get; init; }
public string? Overview { get; init; }
public IReadOnlyList<Text> Overviews { get; init; }
public IReadOnlyList<string> Notes { get; init; } = [];
public string? OriginalLanguageCode { get; init; }
public Rating CommunityRating { get; init; }
/// <summary>
/// First premiere date of the season.
/// </summary>
public DateTime? PremiereDate { get; init; }
/// <summary>
/// Ended date of the season.
/// </summary>
public DateTime? EndDate { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime LastUpdatedAt { get; init; }
private bool? _isAvailable = null;
public bool IsAvailable => _isAvailable ??= EpisodeList.Any(e => e.IsAvailable) || AlternateEpisodesList.Any(e => e.IsAvailable);
public IReadOnlyList<string> Genres { get; init; }
public IReadOnlyList<string> Tags { get; init; }
public IReadOnlyList<string> Studios { get; init; }
public IReadOnlyDictionary<ProviderName, IReadOnlyList<string>> ProductionLocations { get; init; }
public IReadOnlyList<ContentRating> ContentRatings { get; init; }
/// <summary>
/// The inferred days of the week this series airs on.
/// </summary>
/// <value>Each weekday</value>
public IReadOnlyList<DayOfWeek> DaysOfWeek { get; init; }
/// <summary>
/// The yearly seasons this series belongs to.
/// </summary>
public IReadOnlyList<YearlySeason> YearlySeasons { get; init; }
/// <summary>
/// All staff for the season across all episodes.
/// </summary>
public IReadOnlyList<PersonInfo> Staff { get; init; }
/// <summary>
/// A pre-filtered list of normal episodes that belong to this series.
///
/// Ordered by AniDb air-date.
/// </summary>
public IReadOnlyList<EpisodeInfo> EpisodeList { get; init; }
/// <summary>
/// A pre-filtered list of "unknown" episodes that belong to this series.
///
/// Ordered by AniDb air-date.
/// </summary>
public IReadOnlyList<EpisodeInfo> AlternateEpisodesList { get; init; }
/// <summary>
/// A pre-filtered list of "extra" videos that belong to this series.
///
/// Ordered by AniDb air-date.
/// </summary>
public IReadOnlyList<EpisodeInfo> ExtrasList { get; init; }
/// <summary>
/// A pre-filtered list of special episodes without an ExtraType
/// attached.
///
/// Ordered by AniDb episode number.
/// </summary>
public IReadOnlyList<EpisodeInfo> SpecialsList { get; init; }
/// <summary>
/// A list of special episodes that come before normal episodes.
/// </summary>
public IReadOnlySet<string> SpecialsBeforeEpisodes { get; init; }
/// <summary>
/// A dictionary holding mappings for the previous normal episode for every special episode in a series.
/// </summary>
public IReadOnlyDictionary<string, EpisodeInfo> SpecialsAnchors { get; init; }
/// <summary>
/// Related series data available in Shoko.
/// </summary>
public IReadOnlyList<Relation> Relations { get; init; }
/// <summary>
/// Map of related series with type.
/// </summary>
public IReadOnlyDictionary<string, RelationType> RelationMap { get; init; }
#region Shoko Series Metadata
/// <summary>
/// The main Shoko series ID for the season info.
/// </summary>
public string? ShokoSeriesId => ShokoSeries.FirstOrDefault()?.ShokoSeriesId;
/// <summary>
/// The main Shoko group ID for the season info.
/// </summary>
public string? ShokoGroupId => ShokoSeries.FirstOrDefault()?.ShokoGroupId;
/// <summary>
/// All Shoko series linked to the season info.
/// </summary>
public ShokoSeriesInfo[] ShokoSeries { get; init; }
#endregion
#region AniDB Anime Metadata
/// <summary>
/// The main AniDB anime ID for the season info.
/// </summary>
public string? AnidbAnimeId => AnidbAnime.FirstOrDefault()?.AnidbAnimeId;
/// <summary>
/// All AniDB anime linked to the season info.
/// </summary>
public AnidbAnimeInfo[] AnidbAnime { get; init; }
#endregion
#region TMDB Season Metadata
/// <summary>
/// All TMDB seasons linked to the season info.
/// </summary>
public TmdbSeasonInfo[] TmdbSeasons { get; init; }
#endregion
#region TMDB Movie Metadata
/// <summary>
/// All TMDB movies linked to the season info.
/// </summary>
public TmdbMovieInfo[] TmdbMovies { get; init; }
#endregion
public SeasonInfo(
ShokoApiClient client,
ShokoSeries series,
IEnumerable<string> extraIds,
List<EpisodeInfo> episodes,
IReadOnlyList<Relation> relations,
ITmdbEntity? tmdbEntity,
IReadOnlyDictionary<string, SeriesConfiguration> seriesConfigurationMap,
TmdbSeasonInfo[] tmdbSeasons
) {
var seasonId = series.Id;
var relationMap = relations
.Where(r => r.RelatedIDs.Shoko.HasValue)
.DistinctBy(r => r.RelatedIDs.Shoko!.Value)
.ToDictionary(r => r.RelatedIDs.Shoko!.Value.ToString(), r => r.Type);
var specialsBeforeEpisodes = new HashSet<string>();
var specialsAnchorDictionary = new Dictionary<string, EpisodeInfo>();
var specialsList = new List<EpisodeInfo>();
var episodesList = new List<EpisodeInfo>();
var extrasList = new List<EpisodeInfo>();
var altEpisodesList = new List<EpisodeInfo>();
var seasonIdOrder = new string[] { seasonId }.Concat(extraIds).ToList();
// Order the episodes by date.
episodes = episodes
.OrderBy(episode => !episode.AiredAt.HasValue)
.ThenBy(episode => episode.AiredAt)
.ThenBy(e => seasonIdOrder.IndexOf(e.SeasonId))
.ThenBy(episode => episode.Type)
.ThenBy(episode => episode.EpisodeNumber)
.ToList();
// Iterate over the episodes once and store some values for later use.
int index = 0;
int lastNormalEpisode = -1;
foreach (var episode in episodes) {
if (episode.IsHidden)
continue;
var seriesConfiguration = seriesConfigurationMap[episode.SeasonId];
var episodeType = episode.Type is EpisodeType.Episode && seriesConfiguration.EpisodeConversion is SeriesEpisodeConversion.EpisodesAsSpecials ? EpisodeType.Special : episode.Type;
switch (episodeType) {
case EpisodeType.Episode:
episodesList.Add(episode);
lastNormalEpisode = index;
break;
case EpisodeType.Other:
if (episode.ExtraType != null)
extrasList.Add(episode);
else
altEpisodesList.Add(episode);
break;
default:
if (episode.ExtraType != null) {
extrasList.Add(episode);
}
else if (episodeType is EpisodeType.Special && seriesConfiguration.EpisodeConversion is SeriesEpisodeConversion.SpecialsAsEpisodes) {
episodesList.Add(episode);
lastNormalEpisode = index;
}
else if (episodeType is EpisodeType.Special) {
specialsList.Add(episode);
if (lastNormalEpisode == -1) {
specialsBeforeEpisodes.Add(episode.Id);
}
else {
var previousEpisode = episodes
.GetRange(lastNormalEpisode, index - lastNormalEpisode)
.FirstOrDefault(e => e.Type is EpisodeType.Episode && seriesConfiguration.EpisodeConversion is not SeriesEpisodeConversion.EpisodesAsSpecials);
if (previousEpisode != null)
specialsAnchorDictionary[episode.Id] = previousEpisode;
}
}
break;
}
index++;
}
// We order the lists after sorting them into buckets because the bucket
// sort we're doing above have the episodes ordered by air date to get
// the previous episode anchors right.
if (!seriesConfigurationMap[seasonId].OrderByAirdate) {
episodesList = episodesList
.OrderBy(e => seasonIdOrder.IndexOf(e.SeasonId))
.ThenBy(e => e.Type)
.ThenBy(e => e.EpisodeNumber)
.ToList();
altEpisodesList = altEpisodesList
.OrderBy(e => seasonIdOrder.IndexOf(e.SeasonId))
.ThenBy(e => e.Type)
.ThenBy(e => e.EpisodeNumber)
.ToList();
specialsList = specialsList
.OrderBy(e => seasonIdOrder.IndexOf(e.SeasonId))
.ThenBy(e => e.Type)
.ThenBy(e => e.EpisodeNumber)
.ToList();
}
// Replace the normal episodes if we've hidden all the normal episodes and we have at least one
// alternate episode locally.
var type = seriesConfigurationMap[seasonId].Type;
var isCustomType = type != series.AniDB.Type;
if (episodesList.Count == 0 && altEpisodesList.Count > 0) {
// Switch the type from movie to web if we've hidden the main movie, and we have some of the parts.
if (!isCustomType && type == SeriesType.Movie)
type = SeriesType.Web;
episodesList = altEpisodesList;
altEpisodesList = [];
// Re-create the special anchors because the episode list changed.
index = 0;
lastNormalEpisode = -1;
specialsBeforeEpisodes.Clear();
specialsAnchorDictionary.Clear();
foreach (var episode in episodes) {
if (episodesList.Contains(episode)) {
lastNormalEpisode = index;
}
else if (specialsList.Contains(episode)) {
if (lastNormalEpisode == -1) {
specialsBeforeEpisodes.Add(episode.Id);
}
else {
var previousEpisode = episodes
.GetRange(lastNormalEpisode, index - lastNormalEpisode)
.FirstOrDefault(e => episodesList.Contains(e));
if (previousEpisode != null)
specialsAnchorDictionary[episode.Id] = previousEpisode;
}
}
index++;
}
}
// Also switch the type from movie to web if we're hidden the main movies, but the parts are normal episodes.
else if (!isCustomType && type == SeriesType.Movie && episodes.Any(episodeInfo => episodeInfo.IsMainEntry && episodeInfo.IsHidden)) {
type = SeriesType.Web;
}
if (seriesConfigurationMap[seasonId].EpisodeConversion is SeriesEpisodeConversion.SpecialsAsExtraFeaturettes) {
if (specialsList.Count > 0) {
extrasList.AddRange(specialsList);
specialsAnchorDictionary.Clear();
specialsList = [];
}
if (altEpisodesList.Count > 0) {
extrasList.AddRange(altEpisodesList);
altEpisodesList = [];
}
}
var genres = episodes.SelectMany(s => s.Genres).ToList();
var tags = episodes.SelectMany(s => s.Tags).ToList();
AddYearlySeasons(ref genres, ref tags, series.YearlySeasons);
_client = client;
Id = seasonId;
ExtraIds = extraIds.ToArray();
TopLevelShokoGroupId = series.IDs.TopLevelGroup.ToString();
StructureType = seriesConfigurationMap[seasonId].StructureType;
SeasonOrdering = seriesConfigurationMap[seasonId].SeasonOrdering;
SpecialsPlacement = seriesConfigurationMap[seasonId].SpecialsPlacement;
Type = type;
IsMultiEntry = type is SeriesType.Movie && series.Sizes.Total.Episodes > 1;
IsRestricted = series.AniDB.Restricted;
Title = series.Name;
Titles = [
..series.AniDB.Titles,
..(tmdbEntity?.Titles ?? []),
];
var notes = (IReadOnlyList<string>)[];
Overview = series.Description == series.AniDB.Description
? TextUtility.SanitizeAnidbDescription(series.Description)
: series.Description;
Overviews = [
..(!string.IsNullOrEmpty(series.AniDB.Description) ? [
new() {
IsDefault = true,
IsPreferred = string.Equals(series.Description, series.AniDB.Description),
LanguageCode = "en",
Source = "AniDB",
Value = TextUtility.SanitizeAnidbDescription(series.AniDB.Description, out notes),
},
] : Array.Empty<Text>()),
..(tmdbEntity?.Overviews ?? []),
];
Notes = notes;
OriginalLanguageCode = null;
CommunityRating = series.AniDB.Rating;
PremiereDate = series.AniDB.AirDate;
CreatedAt = series.CreatedAt;
LastUpdatedAt = series.LastUpdatedAt;
EndDate = series.AniDB.EndDate;
Genres = genres.Distinct().Order().ToArray();
Tags = tags.Distinct().Order().ToArray();
Studios = episodes.SelectMany(s => s.Studios).Distinct().Order().ToArray();
ProductionLocations = episodes
.SelectMany(sI => sI.ProductionLocations)
.GroupBy(kP => kP.Key, kP => kP.Value)
.ToDictionary(gB => gB.Key, gB => gB.SelectMany(l => l).Distinct().Order().ToList() as IReadOnlyList<string>);
ContentRatings = episodes
.SelectMany(sI => sI.ContentRatings)
.Distinct()
.ToList();
// Movies aren't aired on a schedule like series, so if even if the
// movie series has it's type overridden then we don't want to attempt
// to create a schedule.
if (series.AniDB.Type is SeriesType.Movie) {
DaysOfWeek = [];
}
else {
DaysOfWeek = episodesList
.Select(e => e.AiredAt)
.WhereNotNullOrDefault()
.Distinct()
.Order()
// A single cour season is usually 12-13 episodes, give or take.
.TakeLast(12)
// In case the two first episodes got an early screening in a 12-13 episode single cour anime.
.Skip(2)
.Select(e => e.DayOfWeek)
.Distinct()
.Order()
.ToArray();
}
YearlySeasons = series.YearlySeasons;
Staff = episodes.SelectMany(s => s.Staff).DistinctBy(p => new { p.Type, p.Name, p.Role }).ToArray();
EpisodeList = episodesList;
AlternateEpisodesList = altEpisodesList;
ExtrasList = extrasList;
SpecialsList = specialsList;
SpecialsBeforeEpisodes = specialsBeforeEpisodes;
SpecialsAnchors = specialsAnchorDictionary;
Relations = relations;
RelationMap = relationMap;
ShokoSeries = [
new() {
ShokoSeriesId = series.Id,
ShokoGroupId = series.IDs.ParentGroup.ToString(),
TopLevelShokoGroupId = series.IDs.TopLevelGroup.ToString(),
},
..extraIds.Select(extraId => new ShokoSeriesInfo {
ShokoSeriesId = extraId,
ShokoGroupId = series.IDs.ParentGroup.ToString(),
TopLevelShokoGroupId = series.IDs.TopLevelGroup.ToString(),
}),
];
AnidbAnime = [new() {
AnidbAnimeId = series.AniDB.Id.ToString(),
}];
TmdbSeasons = tmdbSeasons ?? [];
TmdbMovies = [..EpisodeList.SelectMany(eI => eI.TmdbMovies).Distinct()];
}
public SeasonInfo(ShokoApiClient client, TmdbSeason tmdbSeason, TmdbShow tmdbShow, IReadOnlyList<EpisodeInfo> episodes, string? topLevelShokoGroupId, AnidbAnimeInfo[] anidbAnime, ShokoSeriesInfo[] shokoSeries) {
var tags = new List<string>();
var genres = new List<string>();
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
tags.AddRange(tmdbShow.Keywords);
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.TmdbGenres))
tags.AddRange(tmdbShow.Genres);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbKeywords))
genres.AddRange(tmdbShow.Keywords);
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.TmdbGenres))
genres.AddRange(tmdbShow.Genres);
AddYearlySeasons(ref genres, ref tags, tmdbSeason.YearlySeasons);
_client = client;
Id = IdPrefix.TmdbShow + tmdbSeason.Id;
ExtraIds = [];
TopLevelShokoGroupId = topLevelShokoGroupId;
StructureType = SeriesStructureType.TMDB_SeriesAndMovies;
SeasonOrdering = Ordering.OrderType.None;
SpecialsPlacement = Ordering.SpecialOrderType.Excluded;
Type = SeriesType.TV;
IsMultiEntry = true;
IsRestricted = tmdbShow.IsRestricted;
Title = tmdbSeason.Title;
Titles = tmdbSeason.Titles;
Overview = tmdbSeason.Overview;
Overviews = tmdbSeason.Overviews;
OriginalLanguageCode = tmdbShow.OriginalLanguage;
CommunityRating = tmdbShow.UserRating;
if (episodes.Count > 0) {
PremiereDate = episodes[0].AiredAt;
EndDate = (tmdbShow.LastAiredAt.HasValue || tmdbSeason.SeasonNumber < tmdbShow.SeasonCount) && episodes[^1].AiredAt is { } endDate && endDate < DateTime.Now ? endDate : null;
}
CreatedAt = tmdbSeason.CreatedAt;
LastUpdatedAt = tmdbSeason.LastUpdatedAt;
Genres = genres.Distinct().Order().ToArray();
Tags = tags.Distinct().Order().ToArray();
Studios = tmdbShow.Studios.Select(s => s.Name).Distinct().Order().ToArray();
ProductionLocations = new Dictionary<ProviderName, IReadOnlyList<string>>() {
{ ProviderName.TMDB, tmdbShow.ProductionCountries.Values.ToArray() },
};
ContentRatings = tmdbShow.ContentRatings;
DaysOfWeek = episodes
.Select(e => e.AiredAt)
.WhereNotNullOrDefault()
.Distinct()
.Order()
// A single cour season is usually 12-13 episodes, give or take.
.TakeLast(12)
// In case the two first episodes got an early screening in a 12-13 episode single cour anime.
.Skip(2)
.Select(e => e.DayOfWeek)
.Distinct()
.Order()
.ToArray();
YearlySeasons = tmdbSeason.YearlySeasons;
Staff = episodes.SelectMany(s => s.Staff).DistinctBy(p => new { p.Type, p.Name, p.Role }).ToArray();
EpisodeList = tmdbSeason.SeasonNumber is not 0 ? episodes.ToList() : [];
AlternateEpisodesList = [];
ExtrasList = [];
SpecialsList = tmdbSeason.SeasonNumber is 0 ? episodes.ToList() : [];
SpecialsBeforeEpisodes = new HashSet<string>();
SpecialsAnchors = new Dictionary<string, EpisodeInfo>();
Relations = [];
RelationMap = new Dictionary<string, RelationType>();
ShokoSeries = shokoSeries ?? [];
AnidbAnime = anidbAnime ?? [];
TmdbSeasons = [tmdbSeason.ToInfo()];
TmdbMovies = [];
}
public SeasonInfo(ShokoApiClient client, TmdbMovie tmdbMovie, EpisodeInfo episodeInfo, string? topLevelShokoGroupId, AnidbAnimeInfo[] anidbAnime, ShokoSeriesInfo[] shokoSeries) {
var genres = episodeInfo.Genres.ToList();
var tags = episodeInfo.Tags.ToList();
AddYearlySeasons(ref genres, ref tags, tmdbMovie.YearlySeasons);
_client = client;
Id = IdPrefix.TmdbMovie + tmdbMovie.Id.ToString();
ExtraIds = [];
TopLevelShokoGroupId = topLevelShokoGroupId;
StructureType = SeriesStructureType.TMDB_SeriesAndMovies;
SeasonOrdering = Ordering.OrderType.None;
SpecialsPlacement = Ordering.SpecialOrderType.Excluded;
Type = SeriesType.Movie;
IsMultiEntry = false;
IsRestricted = tmdbMovie.IsRestricted;
Title = tmdbMovie.Title;
Titles = tmdbMovie.Titles;
Overview = tmdbMovie.Overview;
Overviews = tmdbMovie.Overviews;
OriginalLanguageCode = tmdbMovie.OriginalLanguage;
CommunityRating = episodeInfo.CommunityRating;
PremiereDate = episodeInfo.AiredAt;
CreatedAt = tmdbMovie.CreatedAt;
LastUpdatedAt = tmdbMovie.LastUpdatedAt;
EndDate = episodeInfo.AiredAt is { } endDate && endDate < DateTime.Now ? endDate : null;
Genres = genres.Distinct().Order().ToArray();
Tags = tags.Distinct().Order().ToArray();
Studios = episodeInfo.Studios;
ProductionLocations = episodeInfo.ProductionLocations;
ContentRatings = episodeInfo.ContentRatings;
DaysOfWeek = [];
YearlySeasons = tmdbMovie.YearlySeasons;
Staff = episodeInfo.Staff;
EpisodeList = [episodeInfo];
AlternateEpisodesList = [];
ExtrasList = [];
SpecialsList = [];
SpecialsBeforeEpisodes = new HashSet<string>();
SpecialsAnchors = new Dictionary<string, EpisodeInfo>();
Relations = [];
RelationMap = new Dictionary<string, RelationType>();
ShokoSeries = shokoSeries;
AnidbAnime = anidbAnime;
TmdbSeasons = [];
TmdbMovies = [new() {
TmdbMovieId = tmdbMovie.Id.ToString(),
TmdbMovieCollectionId = tmdbMovie.CollectionId?.ToString(),
}];
}
public SeasonInfo(ShokoApiClient client, TmdbMovieCollection tmdbMovieCollection, IReadOnlyList<TmdbMovie> movies, IReadOnlyList<EpisodeInfo> episodes, string? topLevelShokoGroupId, AnidbAnimeInfo[] anidbAnime, ShokoSeriesInfo[] shokoSeries) {
var genres = episodes.SelectMany(m => m.Genres).ToList();
var tags = episodes.SelectMany(m => m.Genres).ToList();
AddYearlySeasons(ref genres, ref tags, movies.SelectMany(m => m.YearlySeasons));
_client = client;
Id = IdPrefix.TmdbMovieCollection + tmdbMovieCollection.Id.ToString();
ExtraIds = [];
TopLevelShokoGroupId = topLevelShokoGroupId;
StructureType = SeriesStructureType.TMDB_SeriesAndMovies;
SeasonOrdering = Ordering.OrderType.None;
SpecialsPlacement = Ordering.SpecialOrderType.Excluded;
Type = SeriesType.Movie;
IsMultiEntry = true;
IsRestricted = movies.Any(movie => movie.IsRestricted);
Title = tmdbMovieCollection.Title;
Titles = tmdbMovieCollection.Titles;
Overview = tmdbMovieCollection.Overview;
Overviews = tmdbMovieCollection.Overviews;
OriginalLanguageCode = movies[0].OriginalLanguage;
CommunityRating = episodes[0].CommunityRating;
PremiereDate = episodes[0].AiredAt;
CreatedAt = tmdbMovieCollection.CreatedAt;
LastUpdatedAt = tmdbMovieCollection.LastUpdatedAt;
EndDate = episodes[^1].AiredAt is { } endDate && endDate < DateTime.Now ? endDate : null;
Genres = genres.Distinct().Order().ToArray();
Tags = tags.Distinct().Order().ToArray();
ProductionLocations = episodes
.SelectMany(sI => sI.ProductionLocations)
.GroupBy(kP => kP.Key, kP => kP.Value)
.ToDictionary(gB => gB.Key, gB => gB.SelectMany(l => l).Distinct().Order().ToList() as IReadOnlyList<string>);
ContentRatings = episodes
.SelectMany(sI => sI.ContentRatings)
.Distinct()
.ToList();
DaysOfWeek = [];
YearlySeasons = movies.SelectMany(m => m.YearlySeasons).Distinct().Order().ToArray();
Studios = episodes.SelectMany(m => m.Studios).Distinct().Order().ToArray();
Staff = episodes.SelectMany(s => s.Staff).DistinctBy(p => new { p.Type, p.Name, p.Role }).ToArray();
EpisodeList = [.. episodes];
AlternateEpisodesList = [];
ExtrasList = [];
SpecialsList = [];
SpecialsBeforeEpisodes = new HashSet<string>();
SpecialsAnchors = new Dictionary<string, EpisodeInfo>();
Relations = [];
RelationMap = new Dictionary<string, RelationType>();
ShokoSeries = shokoSeries;
AnidbAnime = anidbAnime;
TmdbSeasons = [];
TmdbMovies = [
..movies.Select(movie => new TmdbMovieInfo {
TmdbMovieId = movie.Id.ToString(),
TmdbMovieCollectionId = movie.CollectionId?.ToString(),
}),
];
}
private void AddYearlySeasons(ref List<string> genres, ref List<string> tags, IEnumerable<YearlySeason> yearlySeasons) {
var seasons = yearlySeasons.Select(season => $"{season.Season} {(season.Season is YearlySeasonName.Winter ? $"{season.Year - 1}/{season.Year.ToString().Substring(2, 2)}" : season.Year)}").ToList();
if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.AllYearlySeasons)) {
tags.AddRange(seasons);
}
else if (Plugin.Instance.Configuration.TagSources.HasFlag(TagFilter.TagSource.AllYearlySeasons) && seasons.Count > 0) {
tags.Add(seasons.First());
}
if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.AllYearlySeasons)) {
genres.AddRange(seasons);
}
else if (Plugin.Instance.Configuration.GenreSources.HasFlag(TagFilter.TagSource.AllYearlySeasons) && seasons.Count > 0) {
genres.Add(seasons.First());
}
}
public async Task<IReadOnlyList<(File file, string seriesId, HashSet<string> episodeIds)>> GetFiles() {
var list = new List<(File file, string seriesId, HashSet<string> episodeIds)>();
if (StructureType is SeriesStructureType.TMDB_SeriesAndMovies) {
if (Id[0] is IdPrefix.TmdbShow) {
var episodes = (await _client.GetTmdbEpisodesInTmdbSeason(Id[1..]).ConfigureAwait(false))
.Select(e => e.Id)
.ToHashSet();
var files = await _client.GetFilesForTmdbSeason(Id[1..]).ConfigureAwait(false);
foreach (var file in files) {
if (file.CrossReferences.Where(x => x.Series.Shoko.HasValue && x.Episodes.Any(e => e.Shoko.HasValue && episodes.Overlaps(e.TMDB.Episode))).ToList() is not { Count: > 0 } xrefList)
continue;
foreach (var xref in xrefList) {
var episodeIds = xref.Episodes
.Where(e => e.Shoko.HasValue && episodes.Overlaps(e.TMDB.Episode))
.SelectMany(e => episodes.Intersect(e.TMDB.Episode))
.Select(e => IdPrefix.TmdbShow + e.ToString())
.ToHashSet();
list.Add((file, xref.Series.Shoko!.Value.ToString(), episodeIds));
}
}
}
else if (Id[0] is IdPrefix.TmdbMovie) {
var files = await _client.GetFilesForTmdbMovie(Id[1..]).ConfigureAwait(false);
var movieId = int.Parse(Id[1..]);
foreach (var file in files) {
if (file.CrossReferences.FirstOrDefault(x => x.Series.Shoko.HasValue && x.Episodes.Any(e => e.Shoko.HasValue && e.TMDB.Movie.Contains(movieId))) is not { } xref)
continue;
list.Add((file, xref.Series.Shoko!.Value.ToString(), [IdPrefix.TmdbMovie + movieId.ToString()]));
}
}
else if (Id[0] is IdPrefix.TmdbMovieCollection) {
var movies = (await _client.GetTmdbMoviesInMovieCollection(Id[1..]).ConfigureAwait(false))
.Select(m => m.Id)
.ToHashSet();
foreach (var episodeInfo in EpisodeList) {
var episodeFiles = await _client.GetFilesForTmdbMovie(episodeInfo.Id[1..]).ConfigureAwait(false);
var movieId = int.Parse(episodeInfo.Id[1..]);
foreach (var file in episodeFiles) {
if (file.CrossReferences.FirstOrDefault(x => x.Series.Shoko.HasValue && x.Episodes.Any(e => e.Shoko.HasValue && e.TMDB.Movie.Contains(movieId))) is not { } xref)
continue;
list.Add((file, xref.Series.Shoko!.Value.ToString(), [IdPrefix.TmdbMovie + movieId.ToString()]));
}
}
}
}
else {
list.AddRange(
(await _client.GetFilesForShokoSeries(Id).ConfigureAwait(false))
.Select(file => (
file,
Id,
file.CrossReferences.FirstOrDefault(x => x.Series.Shoko.HasValue && x.Series.Shoko!.Value.ToString() == Id)?.Episodes.Select(e => e.Shoko!.Value.ToString()).ToHashSet() ?? []
))
);
foreach (var extraId in ExtraIds)
list.AddRange(
(await _client.GetFilesForShokoSeries(extraId).ConfigureAwait(false))
.Select(file => (
file,
extraId,
file.CrossReferences.FirstOrDefault(x => x.Series.Shoko.HasValue && x.Series.Shoko!.Value.ToString() == extraId)?.Episodes.Select(e => e.Shoko!.Value.ToString()).ToHashSet() ?? []
))
);
}
return list;
}
public async Task<Images> GetImages(CancellationToken cancellationToken)
=> Id[0] switch {
IdPrefix.TmdbShow => await _client.GetImagesForTmdbSeason(Id[1..], cancellationToken).ConfigureAwait(false),
IdPrefix.TmdbMovie => await _client.GetImagesForTmdbMovie(Id[1..], cancellationToken).ConfigureAwait(false),
IdPrefix.TmdbMovieCollection => await _client.GetImagesForTmdbMovieCollection(Id[1..], cancellationToken).ConfigureAwait(false),
_ => await _client.GetImagesForShokoSeries(Id, cancellationToken).ConfigureAwait(false),
} ?? new();
public bool IsExtraEpisode(EpisodeInfo? episodeInfo)
=> episodeInfo != null && ExtrasList.Any(eI => eI.Id == episodeInfo.Id);
public bool IsEmpty(int offset = 0) {
// The extra "season" for this season info.
if (offset == 1)
return EpisodeList.Count == 0 || !AlternateEpisodesList.Any(eI => eI.IsAvailable);
// The default "season" for this season info.
var episodeList = EpisodeList.Count == 0 ? AlternateEpisodesList : EpisodeList;
if (!episodeList.Any(eI => eI.IsAvailable))
return false;
return true;
}
}
================================================
FILE: Shokofin/API/Info/Shoko/ShokoEpisodeInfo.cs
================================================
namespace Shokofin.API.Info.Shoko;
public class ShokoEpisodeInfo {
public required string ShokoEpisodeId { get; init; }
public required string ShokoSeriesId { get; init; }
}
================================================
FILE: Shokofin/API/Info/Shoko/ShokoSeriesInfo.cs
================================================
namespace Shokofin.API.Info.Shoko;
public class ShokoSeriesInfo {
public required string ShokoSeriesId { get; init; }
public required string ShokoGroupId { get; init; }
public required string TopLevelShokoGroupId { get; init; }
}
================================================
FILE: Shokofin/API/Info/ShowInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using Microsoft.Extensions.Logging;
using Shokofin.API.Info.AniDB;
using Shokofin.API.Info.Shoko;
using Shokofin.API.Info.TMDB;
using Shokofin.API.Models;
using Shokofin.API.Models.Shoko;
using Shokofin.API.Models.TMDB;
using Shokofin.Events.Interfaces;
using Shokofin.ExternalIds;
using Shokofin.Utils;
using ContentRating = Shokofin.API.Models.ContentRating;
using ContentRatingUtil = Shokofin.Utils.ContentRating;
namespace Shokofin.API.Info;
public class ShowInfo : IExtendedItemInfo {
private readonly ShokoApiClient _client;
public string Id { get; init; }
public string InternalId => ShokoInternalId.SeriesNamespace + Id;
/// <summary>
/// Shoko Group Id used for Collection Support.
/// </summary>
public string? CollectionId { get; init; }
public string Title { get; init; }
public IReadOnlyList<Title> Titles { get; init; }
public string? Overview { get; init; }
public IReadOnlyList<Text> Overviews { get; init; }
public IReadOnlyList<string> Notes { get; init; } = [];
public string? OriginalLanguageCode { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime LastUpdatedAt { get; init; }
/// <summary>
/// Indicates that this show is consistent of only movies.
/// </summary>
public bool IsMovieCollection { get; init; }
/// <summary>
/// Indicates this is a standalone show without a group attached to it.
/// </summary>
public bool IsStandalone { get; init; }
/// <summary>
/// First premiere date of the show.
/// </summary>
public DateTime? PremiereDate { get; init; }
/// <summary>
/// Ended date of the show.
/// </summary>
public DateTime? EndDate { get; init; }
/// <summary>
/// Custom rating of the show.
/// </summary>
public string? CustomRating =>
DefaultSeason.IsRestricted ? "XXX" : null;
/// <summary>
/// Overall community rating of the show.
/// </summary>
public float CommunityRating { get; init; }
/// <summary>
/// All tags from across all seasons.
/// </summary>
public IReadOnlyList<string> Tags { get; init; }
/// <summary>
/// All genres from across all seasons.
/// </summary>
public IReadOnlyList<string> Genres { get; init; }
/// <summary>
/// All production locations from across all seasons.
/// </summary>
public IReadOnlyDictionary<ProviderName, IReadOnlyList<string>> ProductionLocations { get; init; }
public IReadOnlyList<ContentRating> ContentRatings { get; init; }
/// <summary>
/// All studios from across all seasons.
/// </summary>
public IReadOnlyList<string> Studios { get; init; }
/// <summary>
/// The inferred days of the week this series airs on.
/// </summary>
/// <value>Each weekday</value>
public IReadOnlyList<DayOfWeek> DaysOfWeek { get; init; }
/// <summary>
/// The yearly seasons this series belongs to.
/// </summary>
public IReadOnlyList<YearlySeason> YearlySeasons { get; init; }
/// <summary>
/// All staff from across all seasons.
/// </summary>
public IReadOnlyList<PersonInfo> Staff { get; init; }
/// <summary>
/// All seasons.
/// </summary>
public IReadOnlyList<SeasonInfo> SeasonList { get; init; }
/// <summary>
/// The season order dictionary.
/// </summary>
public IReadOnlyDictionary<int, SeasonInfo> SeasonOrderDictionary { get; init; }
/// <summary>
/// A pre-filtered set of special episode ids without an ExtraType
/// attached.
/// </summary>
public IReadOnlyDictionary<string, bool> SpecialsDict { get; init; }
/// <summary>
/// The season number base-number dictionary.
/// </summary>
private Dictionary<string, int> SeasonNumberBaseDictionary { get; init; }
/// <summary>
/// Indicates that the show has specials.
/// </summary>
public bool HasSpecials =>
SpecialsDict.Count > 0;
/// <summary>
/// Indicates that the show has specials with files.
/// </summary>
public bool HasSpecialsWithFiles =>
SpecialsDict.Values.Contains(true);
private bool? _isAvailable = null;
public bool IsAvailable => _isAvailable ??= SeasonOrderDictionary.Values.Any(sI => sI.IsAvailable) || HasSpecialsWithFiles;
/// <summary>
/// The default season for the show.
/// </summary>
public readonly SeasonInfo DefaultSeason;
/// <summary>
/// Episode number padding for file name generation.
/// </summary>
public readonly int EpisodePadding;
#region Shoko Series Metadata
/// <summary>
/// Main Shoko Series Id.
/// </summary>
public string? ShokoSeriesId => ShokoSeries?.FirstOrDefault()?.ShokoSeriesId;
/// <summary>
/// Main Shoko Group Id.
/// </summary>
public string? ShokoGroupId => ShokoSeries?.FirstOrDefault()?.ShokoGroupId;
/// <summary>
/// All Shoko series linked to the show info.
/// </summary>
public ShokoSeriesInfo[] ShokoSeries { get; init; }
#endregion
#region AniDB Anime Metadata
/// <summary>
/// Main AniDB Anime Id.
/// </summary>
public string? AnidbAnimeId => DefaultSeason.StructureType is not Configuration.SeriesStructureType.TMDB_SeriesAndMovies ? AnidbAnime.FirstOrDefault()?.AnidbAnimeId : null;
/// <summary>
/// All AniDB anime linked to the show info.
/// </summary>
public AnidbAnimeInfo[] AnidbAnime { get; init; }
#endregion
#region TMDB Show Metadata
/// <summary>
/// Main TMDB Show Id.
/// </summary>
public string? TmdbShowId => TmdbShows.FirstOrDefault()?.TmdbShowId;
/// <summary>
/// Main TvDB Show Id.
/// </summary>
public string? TvdbShowId => TmdbShows.FirstOrDefault()?.TvdbShowId;
/// <summary>
/// All TMDB shows linked to the show info.
/// </summary>
public TmdbShowInfo[] TmdbShows { get; init; }
#endregion
#region TMDB Movie Metadata
/// <summary>
/// Main TMDB Movie Collection Id.
/// </summary>
public string? TmdbMovieCollectionId => TmdbMovies.FirstOrDefault()?.TmdbMovieCollectionId;
/// <summary>
/// All TMDB movies linked to the show info.
/// </summary>
public TmdbMovieInfo[] TmdbMovies { get; init; }
#endregion
public ShowInfo(ShokoApiClient client, SeasonInfo seasonInfo, TmdbShow? tmdbShow = null, string? collectionId = null) {
var seasonNumberBaseDictionary = new Dictionary<string, int>();
var seasonOrderDictionary = new Dictionary<int, SeasonInfo>();
var seasonNumberOffset = 1;
if (seasonInfo.EpisodeList.Count > 0 || seasonInfo.AlternateEpisodesList.Count > 0)
seasonNumberBaseDictionary.Add(seasonInfo.Id, seasonNumberOffset);
if (seasonInfo.EpisodeList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
if (seasonInfo.AlternateEpisodesList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
_client = client;
Id = seasonInfo.Id;
CollectionId = collectionId ?? seasonInfo.ShokoGroupId!;
IsMovieCollection = seasonInfo.Type is SeriesType.Movie;
IsStandalone = true;
Title = seasonInfo.Title;
Overview = seasonInfo.Overview;
if (tmdbShow != null) {
Titles = seasonInfo.Titles.Where(t => t.Source is not "TMDB").Concat(tmdbShow.Titles).ToList();
Overviews = seasonInfo.Overviews.Where(t => t.Source is not "TMDB").Concat(tmdbShow.Overviews).ToList();
}
else {
Titles = seasonInfo.Titles;
Overviews = seasonInfo.Overviews;
}
Notes = seasonInfo.Notes;
OriginalLanguageCode = seasonInfo.OriginalLanguageCode;
CommunityRating = seasonInfo.CommunityRating.ToFloat(10);
Tags = seasonInfo.Tags;
Genres = seasonInfo.Genres;
PremiereDate = seasonInfo.PremiereDate;
EndDate = seasonInfo.EndDate;
CreatedAt = seasonInfo.CreatedAt;
LastUpdatedAt = seasonInfo.LastUpdatedAt;
ProductionLocations = seasonInfo.ProductionLocations;
ContentRatings = seasonInfo.ContentRatings;
Studios = seasonInfo.Studios;
DaysOfWeek = seasonInfo.DaysOfWeek;
YearlySeasons = seasonInfo.YearlySeasons;
Staff = seasonInfo.Staff;
SeasonList = [seasonInfo];
SeasonNumberBaseDictionary = seasonNumberBaseDictionary;
SeasonOrderDictionary = seasonOrderDictionary;
SpecialsDict = seasonInfo.SpecialsList.ToDictionary(episodeInfo => episodeInfo.Id, episodeInfo => episodeInfo.IsAvailable);
DefaultSeason = seasonInfo;
EpisodePadding = Math.Max(2, (new int[] { seasonInfo.EpisodeList.Count, seasonInfo.AlternateEpisodesList.Count, seasonInfo.SpecialsList.Count }).Max().ToString().Length);
AnidbAnime = seasonInfo.AnidbAnime;
ShokoSeries = seasonInfo.ShokoSeries;
TmdbShows = [..seasonInfo.TmdbSeasons.Select(tmdbSeason => tmdbSeason.ToShowInfo()).Distinct()];
TmdbMovies = seasonInfo.TmdbMovies;
}
public ShowInfo(
ShokoApiClient client,
ILogger logger,
ShokoGroup group,
List<SeasonInfo> seasonList,
ITmdbEntity? tmdbEntity,
bool useGroupIdForCollection
) {
// Order series list based on the main series or first available series.
// Select the targeted id if a group specify a default series.
var foundIndex = -1;
var targetId = group.IDs.MainSeries.ToString();
var orderingSeason = seasonList.FirstOrDefault(s => s.Id == targetId) ?? seasonList[0];
switch (orderingSeason.SeasonOrdering) {
case Ordering.OrderType.Default:
foundIndex = seasonList.FindIndex(s => s.Id == targetId);
break;
case Ordering.OrderType.ReleaseDate:
seasonList = [.. seasonList.OrderBy(s => s?.PremiereDate ?? DateTime.MaxValue)];
foundIndex = 0;
break;
case Ordering.OrderType.Chronological:
case Ordering.OrderType.ChronologicalIgnoreIndirect:
seasonList.Sort(new SeriesInfoRelationComparer(orderingSeason.SeasonOrdering is Ordering.OrderType.Chronological));
foundIndex = seasonList.FindIndex(s => s.Id == targetId);
break;
}
// Fallback to the first series if we can't get a base point for seasons.
var groupId = group.Id;
if (foundIndex == -1) {
logger.LogWarning("Unable to get a base-point for seasons within the group for the filter, so falling back to the first series in the group. This is most likely due to library separation being enabled. (Group={GroupID})", groupId);
foundIndex = 0;
}
var defaultSeason = seasonList[foundIndex];
var specialsSet = new Dictionary<string, bool>();
var seasonOrderDictionary = new Dictionary<int, SeasonInfo>();
var seasonNumberBaseDictionary = new Dictionary<string, int>();
var seasonNumberOffset = 1;
foreach (var seasonInfo in seasonList) {
if (seasonInfo.EpisodeList.Count > 0 || seasonInfo.AlternateEpisodesList.Count > 0)
seasonNumberBaseDictionary.Add(seasonInfo.Id, seasonNumberOffset);
if (seasonInfo.EpisodeList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
if (seasonInfo.AlternateEpisodesList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
foreach (var episodeInfo in seasonInfo.SpecialsList)
specialsSet.Add(episodeInfo.Id, episodeInfo.IsAvailable);
}
var communityRatingSeasons = seasonOrderDictionary
.Where(pair => seasonNumberBaseDictionary.TryGetValue(pair.Value.Id, out var seasonNumber) && seasonNumber == pair.Key && pair.Value.CommunityRating is { Value: > 0 })
.Select(pair => pair.Value)
.ToList();
var anidbRating = ContentRatingUtil.GetCombinedAnidbContentRating(seasonOrderDictionary.Values);
var contentRatings = seasonOrderDictionary.Values
.SelectMany(sI => sI.ContentRatings)
.Where(cR => cR.Source is not "AniDB")
.Distinct()
.ToList();
if (!string.IsNullOrEmpty(anidbRating))
contentRatings.Add(new() {
Rating = anidbRating,
Country = "US",
Language = "en",
Source = "AniDB",
});
_client = client;
Id = defaultSeason.Id;
Title = group.Name;
Titles = [
..defaultSeason.Titles.Where(t => t.Source is "AniDB"),
..(tmdbEntity?.Titles ?? []),
];
Overview = !group.HasCustomDescription
? TextUtility.SanitizeAnidbDescription(group.Description)
: group.Description;
Overviews = [
..defaultSeason.Overviews.Where(t => t.Source is "AniDB"),
..(tmdbEntity?.Overviews ?? []),
];
Notes = defaultSeason.Notes;
CollectionId = useGroupIdForCollection ? groupId : group.IDs.ParentGroup?.ToString();
IsStandalone = false;
PremiereDate = seasonList.Select(s => s.PremiereDate).Where(s => s.HasValue).Min();
EndDate = !seasonList.Any(s => s.PremiereDate.HasValue && s.PremiereDate.Value < DateTime.Now && s.EndDate == null)
? seasonList.Select(s => s.EndDate).Where(s => s.HasValue).Max()
: null;
CreatedAt = seasonList.Select(s => s.CreatedAt).Min();
LastUpdatedAt = seasonList.Select(s => s.LastUpdatedAt).Max();
CommunityRating = communityRatingSeasons.Count > 0
? communityRatingSeasons.Aggregate(0f, (total, seasonInfo) => total + seasonInfo.CommunityRating.ToFloat(10)) / communityRatingSeasons.Count
: 0f;
Genres = seasonList.SelectMany(s => s.Genres).Distinct().ToArray();
Tags = seasonList.SelectMany(s => s.Tags).Distinct().ToArray();
Studios = seasonList.SelectMany(s => s.Studios).Distinct().ToArray();
ProductionLocations = seasonList
.SelectMany(sI => sI.ProductionLocations)
.GroupBy(kP => kP.Key, kP => kP.Value)
.ToDictionary(gB => gB.Key, gB => gB.SelectMany(l => l).Distinct().ToList() as IReadOnlyList<string>);
ContentRatings = contentRatings;
DaysOfWeek = seasonList[^1].DaysOfWeek;
YearlySeasons = seasonList.SelectMany(s => s.YearlySeasons).Distinct().Order().ToArray();
Staff = seasonList.SelectMany(s => s.Staff).DistinctBy(p => new { p.Type, p.Name, p.Role }).ToArray();
SeasonList = seasonList;
SeasonNumberBaseDictionary = seasonNumberBaseDictionary;
SeasonOrderDictionary = seasonOrderDictionary;
SpecialsDict = specialsSet;
DefaultSeason = defaultSeason;
EpisodePadding = Math.Max(2, seasonList.SelectMany(s => new int[] { s.EpisodeList.Count, s.AlternateEpisodesList.Count }).Append(specialsSet.Count).Max().ToString().Length);
AnidbAnime = [..defaultSeason.AnidbAnime.Concat(seasonList.Except([defaultSeason]).SelectMany(s => s.AnidbAnime)).Distinct()];
ShokoSeries = [..defaultSeason.ShokoSeries.Concat(seasonList.Except([defaultSeason]).SelectMany(s => s.ShokoSeries)).Distinct()];
TmdbShows = [
..(tmdbEntity is TmdbShow tmdbShow ? (
new TmdbShowInfo[] { tmdbShow.ToInfo() }
.Concat(seasonList.SelectMany(s => s.TmdbSeasons.Select(tmdbSeason => tmdbSeason.ToShowInfo())))
.Distinct()
) : (
seasonList.SelectMany(s => s.TmdbSeasons.Select(tmdbSeason => tmdbSeason.ToShowInfo())).Distinct()
)),
];
TmdbMovies = [..seasonList.SelectMany(s => s.TmdbMovies).Distinct()];
}
public ShowInfo(ShokoApiClient client, TmdbShow tmdbShow, IReadOnlyList<SeasonInfo> seasonList) {
var defaultSeason = seasonList[0];
var specialsSet = new Dictionary<string, bool>();
var seasonOrderDictionary = new Dictionary<int, SeasonInfo>();
var seasonNumberBaseDictionary = new Dictionary<string, int>();
var seasonNumberOffset = 1;
foreach (var seasonInfo in seasonList) {
if (seasonInfo.EpisodeList.Count > 0 || seasonInfo.AlternateEpisodesList.Count > 0)
seasonNumberBaseDictionary.Add(seasonInfo.Id, seasonNumberOffset);
if (seasonInfo.EpisodeList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
if (seasonInfo.AlternateEpisodesList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
foreach (var episodeInfo in seasonInfo.SpecialsList)
specialsSet.Add(episodeInfo.Id, episodeInfo.IsAvailable);
}
_client = client;
Id = defaultSeason.Id;
if (seasonList.All(seasonInfo => seasonInfo.ShokoSeries.Length is > 0)) {
var shokoGroupIdList = seasonList
.SelectMany(s => s.ShokoSeries)
.GroupBy(s => s.ShokoGroupId)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToList();
if (shokoGroupIdList.Count is 1)
CollectionId = shokoGroupIdList[0];
}
else if (seasonList.All(seasonInfo => !string.IsNullOrEmpty(seasonInfo.TopLevelShokoGroupId))) {
var shokoGroupIdList = seasonList
.GroupBy(s => s.TopLevelShokoGroupId)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToList();
if (shokoGroupIdList.Count is 1)
CollectionId = shokoGroupIdList[0];
}
IsMovieCollection = false;
IsStandalone = true;
Title = tmdbShow.Title;
Titles = tmdbShow.Titles;
Overview = tmdbShow.Overview;
Overviews = tmdbShow.Overviews;
OriginalLanguageCode = tmdbShow.OriginalLanguage;
PremiereDate = tmdbShow.FirstAiredAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
EndDate = tmdbShow.LastAiredAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
CreatedAt = tmdbShow.CreatedAt;
LastUpdatedAt = tmdbShow.LastUpdatedAt;
CommunityRating = tmdbShow.UserRating.ToFloat(10);
Genres = seasonList.SelectMany(s => s.Genres).Distinct().ToArray();
Tags = seasonList.SelectMany(s => s.Tags).Distinct().ToArray();
Studios = seasonList.SelectMany(s => s.Studios).Distinct().ToArray();
ProductionLocations = seasonList
.SelectMany(sI => sI.ProductionLocations)
.GroupBy(kP => kP.Key, kP => kP.Value)
.ToDictionary(gB => gB.Key, gB => gB.SelectMany(l => l).Distinct().ToList() as IReadOnlyList<string>);
ContentRatings = seasonList
.SelectMany(sI => sI.ContentRatings)
.Distinct()
.ToList();
DaysOfWeek = seasonList[^1].DaysOfWeek;
YearlySeasons = seasonList.SelectMany(s => s.YearlySeasons).Distinct().Order().ToArray();
Staff = seasonList.SelectMany(s => s.Staff).DistinctBy(p => new { p.Type, p.Name, p.Role }).ToArray();
SeasonList = seasonList;
SeasonNumberBaseDictionary = seasonNumberBaseDictionary;
SeasonOrderDictionary = seasonOrderDictionary;
SpecialsDict = specialsSet;
DefaultSeason = defaultSeason;
EpisodePadding = Math.Max(2, seasonList.SelectMany(s => new int[] { s.EpisodeList.Count, s.AlternateEpisodesList.Count }).Append(specialsSet.Count).Max().ToString().Length);
AnidbAnime = [..seasonList.SelectMany(s => s.AnidbAnime).Distinct()];
ShokoSeries = [..seasonList.SelectMany(s => s.ShokoSeries).Distinct()];
TmdbShows = [tmdbShow.ToInfo()];
TmdbMovies = [];
}
public ShowInfo(ShokoApiClient client, TmdbMovie tmdbMovie, SeasonInfo seasonInfo) {
var releasedAt = tmdbMovie.ReleasedAt?.ToDateTime(TimeOnly.Parse("00:00:00", CultureInfo.InvariantCulture), DateTimeKind.Local);
_client = client;
Id = seasonInfo.Id;
CollectionId = seasonInfo.ShokoGroupId ?? seasonInfo.TopLevelShokoGroupId;
IsMovieCollection = true;
IsStandalone = true;
Title = tmdbMovie.Title;
Titles = tmdbMovie.Titles;
Overview = tmdbMovie.Overview;
Overviews = tmdbMovie.Overviews;
OriginalLanguageCode = tmdbMovie.OriginalLanguage;
PremiereDate = releasedAt;
EndDate = releasedAt < DateTime.Now ? releasedAt : null;
CreatedAt = tmdbMovie.CreatedAt;
LastUpdatedAt = tmdbMovie.LastUpdatedAt;
CommunityRating = tmdbMovie.UserRating.ToFloat(10);
Genres = seasonInfo.Genres;
Tags = seasonInfo.Tags;
Studios = seasonInfo.Studios;
ProductionLocations = seasonInfo.ProductionLocations;
ContentRatings = seasonInfo.ContentRatings;
DaysOfWeek = [];
YearlySeasons = seasonInfo.YearlySeasons;
Staff = seasonInfo.Staff;
SeasonList = [seasonInfo];
SeasonNumberBaseDictionary = new Dictionary<string, int> { { seasonInfo.Id, 1 } };
SeasonOrderDictionary = new Dictionary<int, SeasonInfo> { { 1, seasonInfo } };
SpecialsDict = new Dictionary<string, bool>();
DefaultSeason = seasonInfo;
EpisodePadding = Math.Max(2, (new int[] { seasonInfo.EpisodeList.Count, seasonInfo.AlternateEpisodesList.Count, seasonInfo.SpecialsList.Count }).Max().ToString().Length);
AnidbAnime = seasonInfo.AnidbAnime;
ShokoSeries = seasonInfo.ShokoSeries;
TmdbShows = [..seasonInfo.TmdbSeasons.Select(tmdbSeason => tmdbSeason.ToShowInfo()).Distinct()];
TmdbMovies = seasonInfo.TmdbMovies;
}
public ShowInfo(ShokoApiClient client, TmdbMovieCollection tmdbMovieCollection, IReadOnlyList<SeasonInfo> seasonList) {
var defaultSeason = seasonList[0];
var specialsSet = new Dictionary<string, bool>();
var seasonOrderDictionary = new Dictionary<int, SeasonInfo>();
var seasonNumberBaseDictionary = new Dictionary<string, int>();
var seasonNumberOffset = 1;
foreach (var seasonInfo in seasonList) {
if (seasonInfo.EpisodeList.Count > 0 || seasonInfo.AlternateEpisodesList.Count > 0)
seasonNumberBaseDictionary.Add(seasonInfo.Id, seasonNumberOffset);
if (seasonInfo.EpisodeList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
if (seasonInfo.AlternateEpisodesList.Count > 0)
seasonOrderDictionary.Add(seasonNumberOffset++, seasonInfo);
foreach (var episodeInfo in seasonInfo.SpecialsList)
specialsSet.Add(episodeInfo.Id, episodeInfo.IsAvailable);
}
var communityRatingSeasons = seasonOrderDictionary
.Where(pair => seasonNumberBaseDictionary.TryGetValue(pair.Value.Id, out var seasonNumber) && seasonNumber == pair.Key && pair.Value.CommunityRating is { Value: > 0 })
.Select(pair => pair.Value)
.ToList();
_client = client;
Id = defaultSeason.Id;
if (seasonList.All(seasonInfo => seasonInfo.ShokoSeries.Length is > 0)) {
var shokoGroupIdList = seasonList
.SelectMany(s => s.ShokoSeries)
.GroupBy(s => s.ShokoGroupId)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToList();
if (shokoGroupIdList.Count is 1)
CollectionId = shokoGroupIdList[0];
}
else if (seasonList.All(seasonInfo => !string.IsNullOrEmpty(seasonInfo.TopLevelShokoGroupId))) {
var shokoGroupIdList = seasonList
.GroupBy(s => s.TopLevelShokoGroupId)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToList();
if (shokoGroupIdList.Count is 1)
CollectionId = shokoGroupIdList[0];
}
IsMovieCollection = seasonList.Count is 1;
IsStandalone = false;
Title = tmdbMovieCollection.Title;
Titles = tmdbMovieCollection.Titles;
Overview = tmdbMovieCollection.Overview;
Overviews = tmdbMovieCollection.Overviews;
OriginalLanguageCode = defaultSeason.OriginalLanguageCode;
PremiereDate = seasonList[0].PremiereDate;
EndDate = seasonList[^1].EndDate;
CreatedAt = tmdbMovieCollection.CreatedAt;
LastUpdatedAt = tmdbMovieCollection.LastUpdatedAt;
CommunityRating = communityRatingSeasons.Count > 0
? communityRatingSeasons.Aggregate(0f, (total, seasonInfo) => total + seasonInfo.CommunityRating.ToFloat(10)) / communityRatingSeasons.Count
: 0f;
Tags = seasonList.SelectMany(s => s.Tags).Distinct().ToArray();
Genres = seasonList.SelectMany(s => s.Genres).Distinct().ToArray();
ProductionLocations = seasonList
.SelectMany(sI => sI.ProductionLocations)
.GroupBy(kP => kP.Key, kP => kP.Value)
.ToDictionary(gB => gB.Key, gB => gB.SelectMany(l => l).Distinct().ToList() as IReadOnlyList<string>);
ContentRatings = seasonList
.SelectMany(sI => sI.ContentRatings)
.Distinct()
.ToList();
Studios = seasonList.SelectMany(s => s.Studios).Distinct().ToArray();
DaysOfWeek = [];
YearlySeasons = seasonList.SelectMany(s => s.YearlySeasons).Distinct().Order().ToArray();
Staff = seasonList.SelectMany(s => s.Staff).DistinctBy(p => new { p.Type, p.Name, p.Role }).ToArray();
SeasonList = seasonList;
SeasonNumberBaseDictionary = seasonNumberBaseDictionary;
SeasonOrderDictionary = seasonOrderDictionary;
SpecialsDict = specialsSet;
DefaultSeason = defaultSeason;
EpisodePadding = Math.Max(2, seasonList.SelectMany(s => new int[] { s.EpisodeList.Count, s.AlternateEpisodesList.Count }).Append(specialsSet.Count).Max().ToString().Length);
AnidbAnime = [..seasonList.SelectMany(s => s.AnidbAnime).Distinct()];
ShokoSeries = [..seasonList.SelectMany(s => s.ShokoSeries).Distinct()];
TmdbShows = [];
TmdbMovies = [..seasonList.SelectMany(s => s.TmdbMovies).Distinct()];
}
public async Task<Images> GetImages(CancellationToken cancellationToken)
=> Id[0] switch {
IdPrefix.TmdbShow => await _client.GetImagesForTmdbShow(TmdbShowId!, cancellationToken).ConfigureAwait(false),
IdPrefix.TmdbMovie => !string.IsNullOrEmpty(TmdbMovieCollectionId)
? await _client.GetImagesForTmdbMovieCollection(TmdbMovieCollectionId, cancellationToken).ConfigureAwait(false)
: await _client.GetImagesForTmdbMovie(Id[1..], cancellationToken).ConfigureAwait(false),
IdPrefix.TmdbMovieCollection => await _client.GetImagesForTmdbMovieCollection(Id[1..], cancellationToken).ConfigureAwait(false),
_ => await _client.GetImagesForShokoSeries(Id, cancellationToken).ConfigureAwait(false),
} ?? new();
public bool IsSpecial(EpisodeInfo episodeInfo)
=> SpecialsDict.ContainsKey(episodeInfo.Id);
public bool TryGetBaseSeasonNumberForSeasonInfo(SeasonInfo season, out int baseSeasonNumber)
=> SeasonNumberBaseDictionary.TryGetValue(season.Id, out baseSeasonNumber);
public int GetBaseSeasonNumberForSeasonInfo(SeasonInfo season)
=> SeasonNumberBaseDictionary.TryGetValue(season.Id, out var baseSeasonNumber) ? baseSeasonNumber : 0;
public SeasonInfo? GetSeasonInfoBySeasonNumber(int seasonNumber)
=> seasonNumber is > 0 && SeasonOrderDictionary.TryGetValue(seasonNumber, out var seasonInfo) ? seasonInfo : null;
}
================================================
FILE: Shokofin/API/Info/TMDB/TmdbEpisodeInfo.cs
================================================
using System.Diagnostics.CodeAnalysis;
namespace Shokofin.API.Info.TMDB;
public class TmdbEpisodeInfo {
public required string TmdbShowId { get; init; }
public required string TmdbAlternateOrderingId { get; init; }
public required string TmdbSeasonId { get; init; }
public required string TmdbEpisodeId { get; init; }
public required string? TvdbEpisodeId { get; init; }
public required int SeasonNumber { get; init; }
public required int EpisodeNumber { get; init; }
public required int? OriginalEpisodeNumber { get; init; }
public required int? OriginalSeasonNumber { get; init; }
[MemberNotNullWhen(true, nameof(OriginalEpisodeNumber))]
[MemberNotNullWhen(true, nameof(OriginalSeasonNumber))]
public bool UsesAlternateOrdering => TmdbShowId != TmdbAlternateOrderingId;
}
================================================
FILE: Shokofin/API/Info/TMDB/TmdbMovieInfo.cs
================================================
using System;
namespace Shokofin.API.Info.TMDB;
public class TmdbMovieInfo : IComparable<TmdbMovieInfo>, IEquatable<TmdbMovieInfo> {
public required string TmdbMovieId { get; init; }
public required string? TmdbMovieCollectionId { get; init; }
public int CompareTo(TmdbMovieInfo? other)
=> other is null ? 1 : TmdbMovieId.CompareTo(other?.TmdbMovieId);
public bool Equals(TmdbMovieInfo? other)
=> other is not null && (ReferenceEquals(this, other) || TmdbMovieId == other.TmdbMovieId);
public override bool Equals(object? obj)
=> Equals(obj as TmdbMovieInfo);
public override int GetHashCode()
=> HashCode.Combine(TmdbMovieId);
}
================================================
FILE: Shokofin/API/Info/TMDB/TmdbSeasonInfo.cs
================================================
namespace Shokofin.API.Info.TMDB;
public class TmdbSeasonInfo {
public required string TmdbShowId { get; init; }
public required string TmdbAlternateOrderingId { get; init; }
public required string TmdbSeasonId { get; init; }
public required int SeasonNumber { get; init; }
public bool UsesAlternateOrdering => TmdbShowId != TmdbAlternateOrderingId;
public TmdbShowInfo ToShowInfo() => new() {
TmdbShowId = TmdbShowId,
TmdbAlternateOrderingId = TmdbAlternateOrderingId,
};
}
================================================
FILE: Shokofin/API/Info/TMDB/TmdbShowInfo.cs
================================================
using System;
namespace Shokofin.API.Info.TMDB;
public class TmdbShowInfo : IComparable<TmdbShowInfo>, IEquatable<TmdbShowInfo> {
public required string TmdbShowId { get; init; }
public required string TmdbAlternateOrderingId { get; init; }
public string? TvdbShowId { get; init; }
public bool UsesAlternateOrdering => TmdbShowId != TmdbAlternateOrderingId;
public int CompareTo(TmdbShowInfo? other)
=> other is null ? 1 : TmdbShowId.CompareTo(other?.TmdbShowId);
public bool Equals(TmdbShowInfo? other)
=> other is not null && (ReferenceEquals(this, other) || TmdbShowId == other.TmdbShowId);
public override bool Equals(object? obj)
=> Equals(obj as TmdbShowInfo);
public override int GetHashCode()
=> HashCode.Combine(TmdbShowId);
}
================================================
FILE: Shokofin/API/Models/AniDB/AnidbAnime.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Shokofin.API.Models.AniDB;
public class AnidbAnime {
/// <summary>
/// AniDB Id
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; set; }
/// <summary>
/// <see cref="Shoko.ShokoSeries"/> Id if the series is available locally.
/// </summary>
[JsonPropertyName("ShokoID")]
public int? ShokoId { get; set; }
/// <summary>
/// Series type. Series, OVA, Movie, etc
/// </summary>
public SeriesType Type { get; set; }
/// <summary>
/// Main Title, usually matches x-jat
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// There should always be at least one of these, the <see cref="Title"/>. May be omitted if needed.
/// </summary>
public IReadOnlyList<Title>? Titles { get; set; }
/// <summary>
/// Description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Restricted content. Mainly porn.
/// </summary>
public bool Restricted { get; set; }
/// <summary>
/// The main or default poster.
/// </summary>
public Image Poster { get; set; } = new();
/// <summary>
/// Number of <see cref="EpisodeType.Episode"/> episodes contained within the series if it's known.
/// </summary>
public int? EpisodeCount { get; set; }
/// <summary>
/// The average rating for the anime. Only available on
/// </summary>
public Rating? Rating { get; set; }
/// <summary>
/// User approval rate for the similar submission. Only available for similar.
/// </summary>
public Rating? UserApproval { get; set; }
/// <summary>
/// Relation type. Only available for relations.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public RelationType? Relation { get; set; }
}
public class AnidbAnimeWithDate : AnidbAnime {
/// <summary>
/// Description.
/// </summary>
public new string Description { get; set; } = string.Empty;
/// <summary>
/// There should always be at least one of these, the <see cref="Title"/>. May be omitted if needed.
/// </summary>
public new List<Title> Titles { get; set; } = [];
/// <summary>
/// The average rating for the anime. Only available on
/// </summary>
public new Rating Rating { get; set; } = new();
/// <summary>
/// Number of <see cref="EpisodeType.Episode"/> episodes contained within the series if it's known.
/// </summary>
public new int EpisodeCount { get; set; }
[JsonIgnore]
private DateTime? InternalAirDate { get; set; } = null;
/// <summary>
/// Air date (2013-02-27). Anything without an air date is going to be missing a lot of info.
/// </summary>
public DateTime? AirDate {
get {
return InternalAirDate;
}
set {
InternalAirDate = value.HasValue && (value.Value == DateTime.UnixEpoch || value.Value == DateTime.MinValue || value.Value == DateTime.MaxValue) ? null : value;
}
}
[JsonIgnore]
private DateTime? InternalEndDate { get; set; } = null;
/// <summary>
/// End date, can be omitted. Omitted means that it's still airing (2013-02-27)
/// </summary>
public DateTime? EndDate {
get {
return InternalEndDate;
}
set {
InternalEndDate = value.HasValue && (value.Value == DateTime.UnixEpoch || value.Value == DateTime.MinValue || value.Value == DateTime.MaxValue) ? null : value;
}
}
}
================================================
FILE: Shokofin/API/Models/AniDB/AnidbEpisode.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Shokofin.API.Info.AniDB;
namespace Shokofin.API.Models.AniDB;
public class AnidbEpisode {
[JsonPropertyName("ID")]
public int Id { get; set; }
[JsonPropertyName("AnimeID")]
public int AnimeId { get; set; }
/// <summary>
/// The duration of the episode.
/// </summary>
public TimeSpan Duration { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public EpisodeType Type { get; set; }
public int EpisodeNumber { get; set; }
public DateTime? AirDate { get; set; }
public IReadOnlyList<Title> Titles { get; set; } = [];
public string Description { get; set; } = string.Empty;
public Rating Rating { get; set; } = new();
public AnidbEpisodeInfo ToInfo() => new() {
AnidbAnimeId = AnimeId.ToString(),
AnidbEpisodeId = Id.ToString(),
EpisodeNumber = EpisodeNumber,
Type = Type,
};
}
================================================
FILE: Shokofin/API/Models/ApiException.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using Shokofin.Extensions;
namespace Shokofin.API.Models;
[Serializable]
public class ApiException : Exception {
private record ValidationResponse {
public Dictionary<string, string[]> errors = [];
public string title = string.Empty;
public HttpStatusCode status = HttpStatusCode.BadRequest;
}
public readonly HttpStatusCode StatusCode;
public readonly ApiExceptionType Type;
public readonly RemoteApiException? Inner;
public readonly Dictionary<string, string[]> ValidationErrors;
public ApiException(HttpStatusCode statusCode, string source, string? message) : base(string.IsNullOrEmpty(message) ? source : $"{source}: {message}") {
StatusCode = statusCode;
Type = ApiExceptionType.Simple;
ValidationErrors = [];
}
protected ApiException(HttpStatusCode statusCode, RemoteApiException inner) : base(inner.Message, inner) {
StatusCode = statusCode;
Type = ApiExceptionType.RemoteException;
Inner = inner;
ValidationErrors = [];
}
protected ApiException(HttpStatusCode statusCode, string source, string? message, Dictionary<string, string[]>? validationErrors = null): base(string.IsNullOrEmpty(message) ? source : $"{source}: {message}") {
StatusCode = statusCode;
Type = ApiExceptionType.ValidationErrors;
ValidationErrors = validationErrors ?? [];
}
public static ApiException FromResponse(HttpResponseMessage response) {
var text = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (text.Length > 0 && text[0] == '{') {
var full = JsonSerializer.Deserialize<ValidationResponse>(text);
var title = full?.title;
var validationErrors = full?.errors;
return new ApiException(response.StatusCode, "ValidationError", title, validationErrors);
}
var index = text.IndexOf("HEADERS");
if (index != -1) {
var (firstLine, lines) = text[..index].TrimEnd().Split('\n');
var (name, splitMessage) = firstLine?.Split(':') ?? [];
var message = string.Join(':', splitMessage).Trim();
var stackTrace = string.Join('\n', lines);
return new ApiException(response.StatusCode, new RemoteApiException(name ?? "InternalServerException", message, stackTrace));
}
return new ApiException(response.StatusCode, response.StatusCode.ToString() + "Exception", text.Split('\n').FirstOrDefault() ?? string.Empty);
}
public class RemoteApiException : Exception {
public RemoteApiException(string source, string message, string stack) : base($"{source}: {message}") {
Source = source;
StackTrace = stack;
}
/// <inheritdoc/>
public override string StackTrace { get; }
}
public enum ApiExceptionType {
Simple = 0,
ValidationErrors = 1,
RemoteException = 2,
}
}
================================================
FILE: Shokofin/API/Models/ApiKey.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class ApiKey {
/// <summary>
/// The Api Key Token.
/// </summary>
[JsonPropertyName("apikey")]
public string Token { get; set; } = string.Empty;
}
================================================
FILE: Shokofin/API/Models/ComponentVersion.cs
================================================
using System;
using System.ComponentModel;
using System.Linq;
using System.Text.Json.Serialization;
using Shokofin.Extensions;
namespace Shokofin.API.Models;
public class ComponentVersionSet {
/// <summary>
/// Shoko.Server version.
/// </summary>
public ComponentVersion Server { get; set; } = new();
}
public class ComponentVersion {
/// <summary>
/// Version number.
/// </summary>
[DefaultValue("1.0.0")]
public string Version { get; set; } = "1.0.0";
/// <summary>
/// Commit SHA.
/// </summary>
public string? Commit { get; set; }
/// <summary>
/// Release channel.
/// </summary>
public ReleaseChannel? ReleaseChannel { get; set; }
/// <summary>
/// Release date.
/// </summary>
public DateTime? ReleaseDate { get; set; } = null;
public override string ToString() {
var extraDetails = new string?[3] {
ReleaseChannel?.ToString(),
Commit?[0..7],
ReleaseDate?.ToUniversalTime().ToString("yyyy-MM-ddThh:mm:ssZ"),
}.Where(s => !string.IsNullOrEmpty(s)).WhereNotNull().Join(", ");
if (extraDetails.Length == 0)
return $"Version {Version}";
return $"Version {Version} ({extraDetails})";
}
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ReleaseChannel {
Stable = 1,
Dev = 2,
Debug = 3,
}
================================================
FILE: Shokofin/API/Models/ContentRating.cs
================================================
using System;
namespace Shokofin.API.Models;
public class ContentRating : IEquatable<ContentRating> {
/// <summary>
/// The content rating for the specified language.
/// </summary>
public string Rating { get; set; } = string.Empty;
/// <summary>
/// The country code the rating applies for.
/// </summary>
public string Country { get; set; } = string.Empty;
/// <summary>
/// The language code the rating applies for.
/// </summary>
public string Language { get; set; } = string.Empty;
/// <summary>
/// The source of the content rating.
/// </summary>
public string Source { get; set; } = string.Empty;
public bool Equals(ContentRating? other)
=> other is not null && (
ReferenceEquals(this, other) || (
Rating == other.Rating &&
Country == other.Country &&
Language == other.Language &&
Source == other.Source
)
);
public override bool Equals(object? obj)
=> obj is ContentRating other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine( Rating, Country, Language, Source);
}
================================================
FILE: Shokofin/API/Models/CrossReference.cs
================================================
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Shokofin.API.Models.Shoko;
namespace Shokofin.API.Models;
public class CrossReference {
/// <summary>
/// The Series IDs
/// </summary>
[JsonPropertyName("SeriesID")]
public SeriesCrossReferenceIDs Series { get; set; } = new();
/// <summary>
/// The Episode IDs
/// </summary>
[JsonPropertyName("EpisodeIDs")]
public List<EpisodeCrossReferenceIDs> Episodes { get; set; } = [];
/// <summary>
/// File episode cross-reference for a series.
/// </summary>
public class EpisodeCrossReferenceIDs {
/// <summary>
/// The Shoko ID, if the local metadata has been created yet.
/// </summary>
[JsonPropertyName("ID")]
public int? Shoko { get; set; }
/// <summary>
/// The AniDB ID.
/// </summary>
public int AniDB { get; set; }
/// <summary>
/// The Movie DataBase (TMDB) Cross-Reference IDs.
/// </summary>
public ShokoEpisode.TmdbEpisodeIDs TMDB { get; set; } = new();
/// <summary>
/// The Release Group ID.
/// </summary>
public int? ReleaseGroup { get; set; }
/// <summary>
/// ED2K hash.
/// </summary>
public string ED2K { get; set; } = string.Empty;
/// <summary>
/// File size.
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// Percentage file is matched to the episode.
/// </summary>
public CrossReferencePercentage Percentage { get; set; } = new();
}
public class CrossReferencePercentage {
/// <summary>
/// File/episode cross-reference percentage range start.
/// </summary>
public int Start { get; set; }
/// <summary>
/// File/episode cross-reference percentage range end.
/// </summary>
public int End { get; set; }
/// <summary>
/// The raw percentage to "group" the cross-references by.
/// </summary>
public int Size { get; set; }
/// <summary>
/// The assumed number of groups in the release, to group the
/// cross-references by.
/// </summary>
public int? Group { get; set; }
}
/// <summary>
/// File series cross-reference.
/// </summary>
public class SeriesCrossReferenceIDs {
/// <summary>
/// The Shoko ID, if the local metadata has been created yet.
/// /// </summary>
[JsonPropertyName("ID")]
public int? Shoko { get; set; }
/// <summary>
/// The AniDB ID.
/// </summary>
public int AniDB { get; set; }
/// <summary>
/// The Movie DataBase (TMDB) Cross-Reference IDs.
/// </summary>
public ShokoSeries.TmdbSeriesIDs TMDB { get; set; } = new();
}
}
================================================
FILE: Shokofin/API/Models/EpisodeType.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EpisodeType {
/// <summary>
/// A catch-all type for future extensions when a provider can't use a current episode type, but knows what the future type should be.
/// </summary>
Other = 2,
/// <summary>
/// The episode type is unknown.
/// </summary>
Unknown = Other,
/// <summary>
/// A normal episode.
/// </summary>
Episode = 1,
/// <summary>
/// A normal episode.
/// </summary>
Normal = Episode,
/// <summary>
/// A special episode.
/// </summary>
Special = 3,
/// <summary>
/// A trailer.
/// </summary>
Trailer = 4,
/// <summary>
/// An opening song, ending song, or other type of credits.
/// </summary>
Credits = 5,
/// <summary>
/// Either an opening-song, or an ending-song.
/// </summary>
ThemeSong = Credits,
/// <summary>
/// Intro, and/or opening-song.
/// </summary>
OpeningSong = 6,
/// <summary>
/// Outro, end-roll, credits, and/or ending-song.
/// </summary>
EndingSong = 7,
/// <summary>
/// AniDB parody type. Where else would this be useful?
/// </summary>
Parody = 8,
/// <summary>
/// A interview tied to the series.
/// </summary>
Interview = 9,
/// <summary>
/// A DVD or BD extra, e.g. BD-menu or deleted scenes.
/// </summary>
Extra = 10,
}
================================================
FILE: Shokofin/API/Models/File.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class File {
/// <summary>
/// The id of the <see cref="File"/>.
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; set; }
/// <summary>
/// The Cross Reference Models for every episode this file belongs to, created in a reverse tree and
/// transformed back into a tree. Series -> Episode such that only episodes that this file is linked to are
/// shown. In many cases, this will have arrays of 1 item
/// </summary>
[JsonPropertyName("SeriesIDs")]
public List<CrossReference> CrossReferences { get; set; } = [];
/// <summary>
/// Indicates this file is marked as a variation in Shoko Server.
/// </summary>
public bool IsVariation { get; set; }
/// <summary>
/// All the <see cref="Location"/>s this <see cref="File"/> is present at.
/// </summary>
public List<Location> Locations { get; set; } = [];
/// <summary>
/// Try to fit this file's resolution to something like 1080p, 480p, etc.
/// </summary>
public string Resolution { get; set; } = string.Empty;
/// <summary>
/// The duration of the file.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// The file creation date of this file.
/// </summary>
[JsonPropertyName("Created")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the file was last imported. Usually is a file only imported once,
/// but there may be exceptions.
/// </summary>
[JsonPropertyName("Imported")]
public DateTime? ImportedAt { get; set; }
[JsonPropertyName("Release")]
public ReleaseInfo? Release { get; set; }
[JsonPropertyName("AniDB")]
public ReleaseInfo? LegacyRelease {
get => Release;
set => Release = value;
}
/// <summary>
/// The size of the file in bytes.
/// </summary>
public long Size { get; set; }
/// <summary>
/// Metadata about the location where a file lies, including the import
/// folder it belongs to and the relative path from the base of the import
/// folder to where it lies.
/// </summary>
public class Location {
/// <summary>
/// File location ID.
/// </summary>
[JsonPropertyName("ID")]
public int? Id { get; set; }
/// <summary>
/// The id of the <see cref="ManagedFolder"/> this <see cref="File"/>
/// resides in.
/// </summary>
[JsonPropertyName("ImportFolderID")]
public int ImportFolderId {
get => ManagedFolderId;
set => ManagedFolderId = value;
}
/// <summary>
/// The id of the <see cref="ManagedFolder"/> this <see cref="File"/>
/// resides in.
/// </summary>
[JsonPropertyName("ManagedFolderID")]
public int ManagedFolderId { get; set; }
/// <summary>
/// The relative path from the base of the <see cref="ManagedFolder"/> to
/// where the <see cref="File"/> lies.
/// </summary>
[JsonPropertyName("RelativePath")]
public string InternalPath { get; set; } = string.Empty;
/// <summary>
/// Cached path for later re-use.
/// </summary>
[JsonIgnore]
private string? CachedPath { get; set; }
/// <summary>
/// The relative path from the base of the <see cref="ManagedFolder"/> to
/// where the <see cref="File"/> lies, with a leading slash applied at
/// the start.
/// </summary>
[JsonIgnore]
public string RelativePath {
get {
if (CachedPath != null)
return CachedPath;
var relativePath = InternalPath
.Replace('/', System.IO.Path.DirectorySeparatorChar)
.Replace('\\', System.IO.Path.DirectorySeparatorChar);
if (relativePath[0] != System.IO.Path.DirectorySeparatorChar)
relativePath = System.IO.Path.DirectorySeparatorChar + relativePath;
return CachedPath = relativePath;
}
}
/// <summary>
/// True if the server can access the the <see cref="Location.RelativePath"/> at
/// the moment of requesting the data.
/// </summary>
[JsonPropertyName("Accessible")]
public bool IsAccessible { get; set; } = false;
}
/// <summary>
/// User stats for the file.
/// </summary>
public class UserStats {
/// <summary>
/// Where to resume the next playback.
/// </summary>
public TimeSpan? ResumePosition { get; set; }
/// <summary>
/// Total number of times the file have been watched.
/// </summary>
public int WatchedCount { get; set; }
/// <summary>
/// When the file was last watched. Will be null if the full is
/// currently marked as unwatched.
/// </summary>
public DateTime? LastWatchedAt { get; set; }
/// <summary>
/// When the entry was last updated.
/// </summary>
public DateTime LastUpdatedAt { get; set; }
/// <summary>
/// True if the <see cref="UserStats"/> object is considered empty.
/// </summary>
public virtual bool IsEmpty {
get => ResumePosition == null && LastWatchedAt == null && WatchedCount == 0;
}
}
}
================================================
FILE: Shokofin/API/Models/IDs.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class IDs {
[JsonPropertyName("ID")]
public int Shoko { get; set; }
}
================================================
FILE: Shokofin/API/Models/Image.cs
================================================
using System;
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class Image {
/// <summary>
/// AniDB, TMDB, etc.
/// </summary>
public ImageSource Source { get; set; } = ImageSource.AniDB;
/// <summary>
/// Poster, Banner, etc.
/// </summary>
public ShokoImageType Type { get; set; } = ShokoImageType.Poster;
/// <summary>
/// The image's id.
/// </summary>
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public int ID { get; set; } = 0;
/// <summary>
/// True if the image is marked as the preferred for the given
/// <see cref="ShokoImageType"/>. Only one preferred is possible for a given
/// <see cref="ShokoImageType"/>.
/// </summary>
[JsonPropertyName("Preferred")]
public bool IsPreferred { get; set; } = false;
/// <summary>
/// True if the image has been disabled. You must explicitly ask for these,
/// for hopefully obvious reasons.
/// </summary>
[JsonPropertyName("Disabled")]
public bool IsDisabled { get; set; } = false;
/// <summary>
/// The language code for the image, if available.
/// </summary>
public string? LanguageCode { get; set; } = null;
/// <summary>
/// Width of the image, if available.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Height of the image, if available.
/// </summary>
public int? Height { get; set; }
/// <summary>
/// The relative path from the image base directory if the image is present
/// on the server.
/// </summary>
[JsonPropertyName("RelativeFilepath")]
public string? LocalPath { get; set; }
/// <summary>
/// True if the image is available.
/// </summary>
[JsonIgnore]
public virtual bool IsAvailable
=> !string.IsNullOrEmpty(LocalPath);
/// <summary>
/// Community rating for the image, if available.
/// </summary>
public Rating? CommunityRating { get; set; }
/// <summary>
/// Json deserialization constructor.
/// </summary>
public Image() { }
/// <summary>
/// Copy constructor.
/// </summary>
public Image(Image image) : this() {
Source = image.Source;
Type = image.Type;
ID = image.ID;
IsPreferred = image.IsPreferred;
IsDisabled = image.IsDisabled;
LanguageCode = image.LanguageCode;
Width = image.Width;
Height = image.Height;
LocalPath = image.LocalPath;
CommunityRating = image.CommunityRating is { } rating ? new(rating) : null;
}
/// <summary>
/// Get an URL to both download the image on the backend and preview it for
/// the clients.
/// </summary>
/// <remarks>
/// May or may not work 100% depending on how the servers and clients are
/// set up, but better than nothing.
/// </remarks>
/// <returns>The image URL</returns>
public string ToURLString(bool internalUrl = false)
=> new Uri(new Uri(internalUrl ? Plugin.Instance.BaseUrl : Web.ImageHostUrl.BaseUrl), $"{(internalUrl ? Plugin.Instance.BasePath : Web.ImageHostUrl.BasePath)}/Shokofin/Host/Image/{Source}/{Type}/{ID}").ToString();
}
/// <summary>
/// Image source.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ImageSource {
/// <summary>
///
/// </summary>
AniDB = 1,
/// <summary>
///
/// </summary>
TMDB = 2,
/// <summary>
///
/// </summary>
Shoko = 100,
}
/// <summary>
/// Image type.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ShokoImageType {
/// <summary>
///
/// </summary>
Poster = 1,
/// <summary>
///
/// </summary>
Banner = 2,
/// <summary>
///
/// </summary>
Thumb = 3,
/// <summary>
///
/// </summary>
Thumbnail = Thumb,
/// <summary>
///
/// </summary>
Fanart = 4,
/// <summary>
///
/// </summary>
Backdrop = Fanart,
/// <summary>
///
/// </summary>
Character = 5,
/// <summary>
///
/// </summary>
Staff = 6,
/// <summary>
/// Clear-text logo.
/// </summary>
Logo = 7,
}
================================================
FILE: Shokofin/API/Models/Images.cs
================================================
using System.Collections.Generic;
namespace Shokofin.API.Models;
public class Images {
public List<Image> Posters { get; set; } = [];
public List<Image> Backdrops { get; set; } = [];
public List<Image> Banners { get; set; } = [];
public List<Image> Logos { get; set; } = [];
}
public class EpisodeImages : Images {
public List<Image> Thumbnails { get; set; } = [];
}
================================================
FILE: Shokofin/API/Models/ListResult.cs
================================================
using System.Collections.Generic;
namespace Shokofin.API.Models;
/// <summary>
/// A list with the total count of <typeparamref name="T"/> entries that
/// match the filter and a sliced or the full list of <typeparamref name="T"/>
/// entries.
/// </summary>
public class ListResult<T> {
/// <summary>
/// Total number of <typeparamref name="T"/> entries that matched the
/// applied filter.
/// </summary>
public int Total { get; set; } = 0;
/// <summary>
/// A sliced page or the whole list of <typeparamref name="T"/> entries.
/// </summary>
public IReadOnlyList<T> List { get; set; } = [];
}
================================================
FILE: Shokofin/API/Models/ManagedFolder.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class ManagedFolder {
/// <summary>
/// The ID of the managed folder.
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; set; }
/// <summary>
/// The friendly name of the managed folder, if any.
/// </summary>
public string? Name { get; set; }
}
================================================
FILE: Shokofin/API/Models/Rating.cs
================================================
namespace Shokofin.API.Models;
public class Rating {
/// <summary>
/// The rating value relative to the <see cref="Rating.MaxValue"/>.
/// </summary>
public decimal Value { get; set; } = 0;
/// <summary>
/// Max value for the rating.
/// </summary>
public int MaxValue { get; set; } = 0;
/// <summary>
/// AniDB, etc.
/// </summary>
public string Source { get; set; } = string.Empty;
/// <summary>
/// number of votes
/// </summary>
public int? Votes { get; set; }
/// <summary>
/// for temporary vs permanent, or any other situations that may arise later
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Json deserialization constructor.
/// </summary>
public Rating() { }
/// <summary>
/// Copy constructor.
/// </summary>
public Rating(Rating rating) {
Value = rating.Value;
MaxValue = rating.MaxValue;
Source = rating.Source;
Votes = rating.Votes;
Type = rating.Type;
}
public float ToFloat(int scale)
=> scale == MaxValue ? (float)Value : (float)((Value * scale) / MaxValue);
}
================================================
FILE: Shokofin/API/Models/Relation.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
/// <summary>
/// Describes relations between two series entries.
/// </summary>
public class Relation {
/// <summary>
/// The IDs of the series.
/// </summary>
public RelationIDs IDs { get; set; } = new();
/// <summary>
/// The IDs of the related series.
/// </summary>
public RelationIDs RelatedIDs { get; set; } = new();
/// <summary>
/// The relation between <see cref="Relation.IDs"/> and <see cref="Relation.RelatedIDs"/>.
/// </summary>
public RelationType Type { get; set; }
/// <summary>
/// AniDB, etc.
/// </summary>
public string Source { get; set; } = "Unknown";
/// <summary>
/// Relation IDs.
/// </summary>
public class RelationIDs {
/// <summary>
/// The ID of the <see cref="Shoko.ShokoSeries"/> entry.
/// </summary>
public int? Shoko { get; set; }
/// <summary>
/// The ID of the <see cref="AniDB.AnidbAnime"/> entry.
/// </summary>
public int? AniDB { get; set; }
}
}
/// <summary>
/// Explains how the main entry relates to the related entry.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum RelationType {
/// <summary>
/// The relation between the entries cannot be explained in simple terms.
/// </summary>
Other = 0,
/// <summary>
/// The entries use the same setting, but follow different stories.
/// </summary>
SameSetting = 1,
/// <summary>
/// The entries use the same base story, but is set in alternate settings.
/// </summary>
AlternativeSetting = 2,
/// <summary>
/// The entries tell the same story in the same settings but are made at different times.
/// </summary>
AlternativeVersion = 3,
/// <summary>
/// The entries tell different stories in different settings but otherwise shares some character(s).
/// </summary>
SharedCharacters = 4,
/// <summary>
/// The first story either continues, or expands upon the story of the related entry.
/// </summary>
Prequel = 20,
/// <summary>
/// The related entry is the main-story for the main entry, which is a side-story.
/// </summary>
MainStory = 21,
/// <summary>
/// The related entry is a longer version of the summarized events in the main entry.
/// </summary>
FullStory = 22,
/// <summary>
/// The related entry either continues, or expands upon the story of the main entry.
/// </summary>
Sequel = 40,
/// <summary>
/// The related entry is a side-story for the main entry, which is the main-story.
/// </summary>
SideStory = 41,
/// <summary>
/// The related entry summarizes the events of the story in the main entry.
/// </summary>
Summary = 42,
}
================================================
FILE: Shokofin/API/Models/ReleaseGroup.cs
================================================
using System.Text.Json.Serialization;
using Shokofin.API.Converters;
namespace Shokofin.API.Models;
public class ReleaseGroup {
/// <summary>
/// The AniDB Release Group ID (e.g. 1)
/// /// </summary>
[JsonPropertyName("ID"), JsonConverter(typeof(JsonAutoStringConverter))]
public string? Id { get; set; }
/// <summary>
/// The release group's Name (e.g. "Unlimited Translation Works")
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The release group's Name (e.g. "UTW")
/// </summary>
public string? ShortName { get; set; }
/// <summary>
/// The release group's Source (e.g. "AniDB")
/// </summary>
public string? Source { get; set; }
}
================================================
FILE: Shokofin/API/Models/ReleaseInfo.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class ReleaseInfo {
/// <summary>
/// Blu-ray, DVD, LD, TV, etc..
/// </summary>
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
public ReleaseSource Source { get; set; }
/// <summary>
/// The Release Group.
/// </summary>
[JsonInclude, JsonPropertyName("Group")]
public ReleaseGroup? Group { get; set; }
/// <summary>
/// The Release Group.
/// </summary>
[JsonInclude, JsonPropertyName("ReleaseGroup")]
public ReleaseGroup? LegacyGroup {
get => Group;
set => Group = value;
}
/// <summary>
/// The file's version.
/// </summary>
public int Version { get; set; }
}
================================================
FILE: Shokofin/API/Models/ReleaseSource.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ReleaseSource {
Unknown = 0,
Other = 1,
TV = 2,
DVD = 3,
BluRay = 4,
Web = 5,
VHS = 6,
VCD = 7,
LaserDisc = 8,
Camera = 9
}
================================================
FILE: Shokofin/API/Models/Role.cs
================================================
using System;
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
public class Role : IEquatable<Role> {
/// <summary>
/// Extra info about the role. For example, role can be voice actor, while role_details is Main Character
/// </summary>
[JsonPropertyName("RoleDetails")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// The role that the staff plays, cv, writer, director, etc
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonPropertyName("RoleName")]
public CreatorRoleType Type { get; set; }
/// <summary>
/// Most will be Japanese. Once AniList is in, it will have multiple options
/// </summary>
public string? Language { get; set; }
public Person Staff { get; set; } = new();
/// <summary>
/// The character played, the <see cref="Role.Type"/> is of type
/// <see cref="CreatorRoleType.Actor"/>.
/// </summary>
public Person? Character { get; set; }
public override bool Equals(object? obj)
=> Equals(obj as Role);
public bool Equals(Role? other) {
if (other is null)
return false;
return string.Equals(Name, other.Name, StringComparison.Ordinal) &&
Type == other.Type &&
string.Equals(Language, other.Language, StringComparison.Ordinal) &&
Staff.Equals(other.Staff) &&
(Character is null ? other.Character is null : Character.Equals(other.Character));
}
public override int GetHashCode()
=> HashCode.Combine(Name, Type, Language, Staff, Character);
public class Person : IEquatable<Person> {
/// <summary>
/// Id of the person.
/// </summary>
[JsonPropertyName("ID")]
public int? Id { get; set; }
/// <summary>
/// Whether the object is a person, company or collab. Only set for AniDB creators.
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Main Name, romanized if needed
/// ex. John Smith
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Alternate Name, this can be any other name, whether kanji, an alias, etc
/// ex. 澤野弘之
/// </summary>
public string? AlternateName { get; set; }
/// <summary>
/// A description, bio, etc
/// ex. John Smith was born September 12, 1980 in Tokyo, Japan. He is a composer and arranger.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Visual representation of the character or staff. Usually a profile
/// picture.
/// </summary>
public Image Image { get; set; } = new();
public override bool Equals(object? obj)
=> Equals(obj as Person);
public bool Equals(Person? other) {
if (other is null)
return false;
return Id == other.Id &&
string.Equals(Name, other.Name, StringComparison.Ordinal) &&
string.Equals(Description, other.Description, StringComparison.Ordinal) &&
string.Equals(AlternateName, other.AlternateName, StringComparison.Ordinal);
}
public override int GetHashCode()
=> HashCode.Combine(Id, Name, Description, AlternateName);
}
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CreatorRoleType {
/// <summary>
/// Voice actor or voice actress.
/// </summary>
Actor,
/// <summary>
/// Synonym of <see cref="CreatorRoleType.Actor"/> for backwards compatibility for a few server versions.
/// </summary>
Seiyuu = Actor,
/// <summary>
/// This can be anything involved in writing the show.
/// </summary>
Staff,
/// <summary>
/// The studio responsible for publishing the show.
/// </summary>
Studio,
/// <summary>
/// The main producer(s) for the show.
/// </summary>
Producer,
/// <summary>
/// Direction.
/// </summary>
Director,
/// <summary>
/// Series Composition.
/// </summary>
SeriesComposer,
/// <summary>
/// Character Design.
/// </summary>
CharacterDesign,
/// <summary>
/// Music composer.
/// </summary>
Music,
/// <summary>
/// Responsible for the creation of the source work this show is derived from.
/// </summary>
SourceWork,
}
================================================
FILE: Shokofin/API/Models/SeriesType.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SeriesType {
/// <summary>
/// Only for use with <see cref="Configuration.SeriesConfiguration"/>.
/// </summary>
None,
/// <summary>
/// The series type is unknown as of yet. This may be updated in the future.
/// </summary>
Unknown,
/// <summary>
/// A catch-all type for future extensions when a provider can't use a current episode type, but knows what the future type should be.
/// </summary>
Other,
/// <summary>
/// Standard TV series.
/// </summary>
TV,
/// <summary>
/// TV special.
/// </summary>
TVSpecial,
/// <summary>
/// Original Net Animations (ONAs), AKA standalone releases that aired on the web.
/// </summary>
Web,
/// <summary>
/// All movies, regardless of source (e.g. web or theater)
/// </summary>
Movie,
/// <summary>
/// Original Video Animations, AKA standalone releases that don't air on TV or the web.
/// </summary>
OVA,
/// <summary>
/// Standalone music videos.
/// </summary>
MusicVideo,
}
================================================
FILE: Shokofin/API/Models/Shoko/ShokoEpisode.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Shokofin.API.Info.Shoko;
using Shokofin.API.Models.AniDB;
namespace Shokofin.API.Models.Shoko;
public class ShokoEpisode {
public string Id => IDs.Shoko.ToString();
/// <summary>
/// All identifiers related to the episode entry, e.g. the Shoko, AniDB,
/// TMDB, etc.
/// </summary>
public EpisodeIDs IDs { get; set; } = new();
/// <summary>
/// The preferred name of the episode based on the selected episode language
/// settings on the server.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The preferred description of the episode based on the selected episode
/// language settings on the server.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The duration of the episode.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Indicates the episode is hidden.
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Number of files linked to the episode.
/// </summary>
/// <value></value>
public int Size { get; set; }
/// <summary>
/// The <see cref="AnidbEpisode"/>, if <see cref="DataSource.AniDB"/> is
/// included in the data to add.
/// </summary>
public AnidbEpisode AniDB { get; set; } = new();
/// <summary>
/// File cross-references for the episode.
/// </summary>
public List<CrossReference.EpisodeCrossReferenceIDs> CrossReferences { get; set; } = [];
/// <summary>
/// When the episode entry was created.
/// </summary>
[JsonPropertyName("Created")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the episode entry was last updated.
/// </summary>
[JsonPropertyName("Updated")]
public DateTime LastUpdatedAt { get; set; }
public ShokoEpisodeInfo ToInfo() => new() {
ShokoSeriesId = IDs.ParentSeries.ToString(),
ShokoEpisodeId = Id,
};
public class EpisodeIDs : IDs {
public int ParentSeries { get; set; }
#if DEBUG
public int AniDB { get; set; }
public List<int> TvDB { get; set; } = [];
public List<string> IMDB { get; set; } = [];
#endif
public TmdbEpisodeIDs TMDB { get; init; } = new();
}
public class TmdbEpisodeIDs {
public List<int> Episode { get; init; } = [];
public List<int> Movie { get; init; } = [];
#if DEBUG
public List<int> Show { get; init; } = [];
#endif
}
}
================================================
FILE: Shokofin/API/Models/Shoko/ShokoGroup.cs
================================================
using System;
using System.Text.Json.Serialization;
namespace Shokofin.API.Models.Shoko;
public class ShokoGroup {
public string Id => IDs.Shoko.ToString();
public GroupIDs IDs { get; set; } = new();
public string Name { get; set; } = string.Empty;
#if DEBUG
public string SortName { get; set; } = string.Empty;
public bool HasCustomName { get; set; }
#endif
public string Description { get; set; } = string.Empty;
public bool HasCustomDescription { get; set; }
public int Size { get; set; }
public GroupSizes Sizes { get; set; } = new();
/// <summary>
/// When the group entry was created.
/// </summary>
[JsonPropertyName("Created")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the group entry was last updated.
/// </summary>
[JsonPropertyName("Updated")]
public DateTime LastUpdatedAt { get; set; }
public class GroupIDs : IDs {
public int MainSeries { get; set; }
public int? ParentGroup { get; set; }
public int TopLevelGroup { get; set; }
}
/// <summary>
/// Downloaded, Watched, Total, etc
/// </summary>
public class GroupSizes : ShokoSeries.SeriesSizes {
/// <summary>
/// Number of direct sub-groups within the group.
/// /// </summary>
/// <value></value>
public int SubGroups { get; set; }
#if DEBUG
/// <summary>
/// Count of the different series types within the group.
/// </summary>
public SeriesTypeCounts SeriesTypes { get; set; } = new();
public class SeriesTypeCounts {
public int Unknown { get; set; }
public int Other { get; set; }
public int TV { get; set; }
public int TVSpecial { get; set; }
public int Web { get; set; }
public int Movie { get; set; }
public int OVA { get; set; }
}
#endif
}
}
================================================
FILE: Shokofin/API/Models/Shoko/ShokoSeries.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Shokofin.API.Models.AniDB;
namespace Shokofin.API.Models.Shoko;
public class ShokoSeries {
public string Id => IDs.Shoko.ToString();
/// <summary>
/// All identifiers related to the series entry, e.g. the Shoko, AniDB,
/// TMDB, etc.
/// </summary>
public SeriesIDs IDs { get; set; } = new();
/// <summary>
/// The preferred name of the series based on the selected series language
/// settings on the server.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The preferred description of the series based on the selected series
/// language settings on the server.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The yearly seasons this series belongs to.
/// </summary>
public List<YearlySeason> YearlySeasons { get; set; } = [];
/// <summary>
/// The AniDB entry.
/// </summary>
public AnidbAnimeWithDate AniDB { get; set; } = new();
/// <summary>
/// Different size metrics for the series.
/// </summary>
public SeriesSizes Sizes { get; set; } = new();
/// <summary>
/// When the series entry was created during the process of the first file
/// being added to Shoko.
/// </summary>
[JsonPropertyName("Created")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the series entry was last updated.
/// </summary>
[JsonPropertyName("Updated")]
public DateTime LastUpdatedAt { get; set; }
public class SeriesIDs : IDs {
/// <summary>
/// The ID of the direct parent group, if it has one.
/// </summary>
public int ParentGroup { get; set; } = 0;
/// <summary>
/// The ID of the top-level (ancestor) group this series belongs to.
/// </summary>
public int TopLevelGroup { get; set; } = 0;
/// <summary>
/// The AniDB ID
/// </summary>
public int AniDB { get; set; } = 0;
/// <summary>
/// The TvDB IDs
/// </summary>
public List<int> TvDB { get; set; } = [];
/// <summary>
/// The IMDB Movie IDs.
/// </summary>
public List<string> IMDB { get; set; } = [];
/// <summary>
/// The Movie Database (TMDB) IDs.
/// </summary>
public TmdbSeriesIDs TMDB { get; set; } = new();
}
public class TmdbSeriesIDs {
public List<int> Movie { get; init; } = [];
public List<int> Show { get; init; } = [];
}
/// <summary>
/// Different size metrics for the series.
/// </summary>
public class SeriesSizes {
#if DEBUG
/// <summary>
/// Count of hidden episodes, be it available or missing.
/// </summary>
public int Hidden { get; set; }
#endif
/// <summary>
/// Combined count of all files across all file sources within the series or group.
/// </summary>
public int Files =>
FileSources.Unknown +
FileSources.Other +
FileSources.TV +
FileSources.DVD +
FileSources.BluRay +
FileSources.Web +
FileSources.VHS +
FileSources.VCD +
FileSources.LaserDisc +
FileSources.Camera;
/// <summary>
/// Counts of each file source type available within the local collection
/// </summary>
public FileSourceCounts FileSources { get; set; } = new();
#if DEBUG
/// <summary>
/// What is downloaded and available
/// </summary>
public EpisodeTypeCounts Local { get; set; } = new();
/// <summary>
/// What is local and watched.
/// </summary>
public EpisodeTypeCounts Watched { get; set; } = new();
#endif
/// <summary>
/// Total count of each type
/// </summary>
public EpisodeTypeCounts Total { get; set; } = new();
/// <summary>
/// Lists the count of each type of episode.
/// </summary>
public class EpisodeTypeCounts {
public int Episodes { get; set; }
#if DEBUG
public int Specials { get; set; }
public int Credits { get; set; }
public int Trailers { get; set; }
public int Parodies { get; set; }
public int Others { get; set; }
#endif
}
public class FileSourceCounts {
public int Unknown { get; set; }
public int Other { get; set; }
public int TV { get; set; }
public int DVD { get; set; }
public int BluRay { get; set; }
public int Web { get; set; }
public int VHS { get; set; }
public int VCD { get; set; }
public int LaserDisc { get; set; }
public int Camera { get; set; }
}
}
}
================================================
FILE: Shokofin/API/Models/Studio.cs
================================================
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Shokofin.API.Models;
/// <summary>
/// APIv3 Studio Data Transfer Object (DTO).
/// </summary>
public class Studio {
/// <summary>
/// Studio ID relative to the <see cref="Source"/>.
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; init; }
/// <summary>
/// The name of the studio.
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// The country the studio originates from.
/// </summary>
public string CountryOfOrigin { get; init; } = string.Empty;
/// <summary>
/// Entities produced by the studio in the local collection, both movies
/// and/or shows.
/// </summary>
public int Size { get; init; }
/// <summary>
/// Logos used by the studio.
/// </summary>
public IReadOnlyList<Image> Logos { get; init; } = [];
/// <summary>
/// The source of which the studio metadata belongs to.
/// </summary>
public string Source { get; init; } = string.Empty;
}
================================================
FILE: Shokofin/API/Models/TMDB/AlternateOrderingType.cs
================================================
namespace Shokofin.API.Models.TMDB;
public enum AlternateOrderingType {
Unknown = 0,
OriginalAirDate = 1,
Absolute = 2,
DVD = 3,
Digital = 4,
StoryArc = 5,
Production = 6,
TV = 7,
}
================================================
FILE: Shokofin/API/Models/TMDB/ITmdbEntity.cs
================================================
using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
namespace Shokofin.API.Models.TMDB;
public interface ITmdbEntity {
string Id { get; }
BaseItemKind Kind { get; }
/// <summary>
/// Preferred title based upon Shoko's title preference.
/// </summary>
string Title { get; }
/// <summary>
/// All available titles for the entity, if they should be included.
/// </summary>
IReadOnlyList<Title> Titles { get; }
/// <summary>
/// Preferred overview based upon description preference.
/// </summary>
string Overview { get; }
/// <summary>
/// All available overviews for the entity, if they should be included.
/// </summary>
IReadOnlyList<Text> Overviews { get; }
/// <summary>
/// When the local metadata was first created.
/// </summary>
DateTime CreatedAt { get; }
/// <summary>
/// When the local metadata was last updated with new changes from the
/// remote.
/// </summary>
DateTime LastUpdatedAt { get; }
}
================================================
FILE: Shokofin/API/Models/TMDB/ITmdbParentEntity.cs
================================================
using System.Collections.Generic;
namespace Shokofin.API.Models.TMDB;
public interface ITmdbParentEntity : ITmdbEntity {
/// <summary>
/// Original language the entity was shot in.
/// /// </summary>
string OriginalLanguage { get; }
/// <summary>
/// Indicates the entity is restricted to an age group above the legal age,
/// because it's a pornography.
/// </summary>
bool IsRestricted { get; }
/// <summary>
/// Genres.
/// </summary>
IReadOnlyList<string> Genres { get; }
/// <summary>
/// Keywords.
/// </summary>
IReadOnlyList<string> Keywords { get; }
/// <summary>
/// User rating of the entity from TMDB users.
/// </summary>
Rating UserRating { get; }
/// <summary>
/// Content ratings for different countries for this entity.
/// </summary>
IReadOnlyList<ContentRating> ContentRatings { get; }
/// <summary>
/// The production countries.
/// </summary>
IReadOnlyDictionary<string, string> ProductionCountries { get; }
/// <summary>
/// The production companies (studios) that produced the entity.
/// </summary>
IReadOnlyList<Studio> Studios { get; }
}
================================================
FILE: Shokofin/API/Models/TMDB/TmdbEpisode.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Shokofin.API.Info.TMDB;
namespace Shokofin.API.Models.TMDB;
/// <summary>
/// APIv3 The Movie DataBase (TMDB) Episode Data Transfer Object (DTO).
/// </summary>
public class TmdbEpisode : ITmdbEntity {
/// <summary>
/// TMDB Episode ID.
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; set; }
/// <summary>
/// TMDB Season ID.
/// </summary>
[JsonPropertyName("SeasonID")]
public string SeasonId { get; set; } = string.Empty;
/// <summary>
/// TMDB Show ID.
/// </summary>
[JsonPropertyName("ShowID")]
public int ShowId { get; set; }
/// <summary>
/// The ID of the alternate ordering currently in use for the episode.
/// </summary>
[JsonPropertyName("AlternateOrderingID")]
public string AlternateOrderingId { get; init; } = string.Empty;
/// <summary>
/// TVDB Episode ID, if available.
/// </summary>
[JsonPropertyName("TvdbEpisodeID")]
public int? TvdbEpisodeId { get; set; }
/// <summary>
/// Preferred title based upon episode title preference.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// All available titles for the episode, if they should be included.
/// </summary>
public IReadOnlyList<Title> Titles { get; set; } = [];
/// <summary>
/// Preferred overview based upon episode title preference.
/// </summary>
public string Overview { get; set; } = string.Empty;
/// <summary>
/// All available overviews for the episode, if they should be included.
/// </summary>
public IReadOnlyList<Text> Overviews { get; set; } = [];
/// <summary>
/// The episode number for the main ordering or alternate ordering in use.
/// </summary>
public int EpisodeNumber { get; set; }
/// <summary>
/// The season number for the main ordering or alternate ordering in use.
/// </summary>
public int SeasonNumber { get; set; }
/// <summary>
/// User rating of the episode from TMDB users.
/// </summary>
public Rating UserRating { get; set; } = new();
/// <summary>
/// The episode run-time, if it is known.
/// </summary>
public TimeSpan? Runtime { get; set; }
/// <summary>
/// The cast that have worked on this show across all episodes and all seasons.
/// </summary>
public IReadOnlyList<Role> Cast { get; set; } = [];
/// <summary>
/// The crew that have worked on this show across all episodes and all seasons.
/// </summary>
public IReadOnlyList<Role> Crew { get; set; } = [];
/// <summary>
/// All available ordering for the episode, if they should be included.
/// </summary>
public IReadOnlyList<OrderingInformation> Ordering { get; init; } = [];
/// <summary>
/// TMDB episode to file cross-references.
/// </summary>
public IReadOnlyList<CrossReference> FileCrossReferences { get; set; } = [];
/// <summary>
/// The date the episode first aired, if it is known.
/// </summary>
public DateOnly? AiredAt { get; set; }
/// <summary>
/// When the local metadata was first created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the local metadata was last updated with new changes from the
/// remote.
/// </summary>
public DateTime LastUpdatedAt { get; set; }
string ITmdbEntity.Id => Id.ToString();
BaseItemKind ITmdbEntity.Kind => BaseItemKind.Episode;
public TmdbEpisodeInfo ToInfo() => new() {
TmdbShowId = ShowId.ToString(),
TmdbAlternateOrderingId = AlternateOrderingId.ToString(),
TmdbSeasonId = SeasonId.ToString(),
TmdbEpisodeId = Id.ToString(),
SeasonNumber = SeasonNumber,
EpisodeNumber = EpisodeNumber,
TvdbEpisodeId = TvdbEpisodeId?.ToString(),
OriginalEpisodeNumber = AlternateOrderingId != ShowId.ToString() ? Ordering.First(o => o.IsDefault).EpisodeNumber : null,
OriginalSeasonNumber = AlternateOrderingId != ShowId.ToString() ? Ordering.First(o => o.IsDefault).SeasonNumber : null,
};
public class OrderingInformation {
#if DEBUG
/// <summary>
/// The ordering ID.
/// </summary>
public string OrderingID { get; set; } = string.Empty;
/// <summary>
/// The alternate ordering type. Will not be set if the main ordering is
/// used.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public AlternateOrderingType? OrderingType { get; set; }
/// <summary>
/// English name of the alternate ordering scheme.
/// </summary>
public string OrderingName { get; set; } = string.Empty;
/// <summary>
/// The season id. Will be a stringified integer for the main ordering,
/// or a hex id any alternate ordering.
/// </summary>
public string SeasonID { get; set; } = string.Empty;
/// <summary>
/// English name of the season.
/// </summary>
public string SeasonName { get; set; } = string.Empty;
#endif
/// <summary>
/// The season number for the ordering.
/// </summary>
public int SeasonNumber { get; set; }
/// <summary>
/// The episode number for the ordering.
/// </summary>
public int EpisodeNumber { get; set; }
/// <summary>
/// Indicates the current ordering is the default ordering for the episode.
/// </summary>
public bool IsDefault { get; set; }
#if DEBUG
/// <summary>
/// Indicates the current ordering is the preferred ordering for the episode.
/// </summary>
public bool IsPreferred { get; set; }
/// <summary>
/// Indicates the current ordering is in use for the episode.
/// </summary>
public bool InUse { get; set; }
#endif
}
}
================================================
FILE: Shokofin/API/Models/TMDB/TmdbEpisodeCrossReference.cs
================================================
using System.Text.Json.Serialization;
namespace Shokofin.API.Models.TMDB;
/// <summary>
/// APIv3 The Movie DataBase (TMDB) Episode Cross-Reference Data Transfer Object (DTO).
/// </summary>
public class TmdbEpisodeCrossReference {
/// <summary>
/// AniDB Anime ID.
/// </summary>
[JsonPropertyName("AnidbAnimeID")]
public int AnidbAnimeId { get; init; }
/// <summary>
/// AniDB Episode ID.
/// </summary>
[JsonPropertyName("AnidbEpisodeID")]
public int AnidbEpisodeId { get; init; }
/// <summary>
/// TMDB Show ID.
/// </summary>
[JsonPropertyName("TmdbShowID")]
public int TmdbShowId { get; init; }
/// <summary>
/// TMDB Episode ID. Will be <c>0</c> if the <see cref="AnidbEpisodeID"/>
/// is not mapped to a TMDB Episode yet.
/// </summary>
[JsonPropertyName("TmdbEpisodeID")]
public int TmdbEpisodeId { get; init; }
/// <summary>
/// The index to order the cross-references if multiple references
/// exists for the same anidb or tmdb episode.
/// </summary>
public int Index { get; init; }
/// <summary>
/// The match rating.
/// </summary>
public string Rating { get; init; } = string.Empty;
}
================================================
FILE: Shokofin/API/Models/TMDB/TmdbMovie.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Shokofin.API.Info.TMDB;
namespace Shokofin.API.Models.TMDB;
public class TmdbMovie : ITmdbParentEntity {
/// <summary>
/// TMDB Movie ID.
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; set; }
/// <summary>
/// TMDB Movie Collection ID, if the movie is in a movie collection on TMDB.
/// </summary>
[JsonPropertyName("CollectionID")]
public int? CollectionId { get; set; }
/// <summary>
/// IMDB Movie ID, if available.
/// </summary>
[JsonPropertyName("ImdbMovieID")]
public string? ImdbMovieId { get; set; }
/// <summary>
/// Preferred title based upon series title preference.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// All available titles for the movie, if they should be included.
/// </summary>
public IReadOnlyList<Title> Titles { get; set; } = [];
/// <summary>
/// Preferred overview based upon description preference.
/// </summary>
public string Overview { get; set; } = string.Empty;
/// <summary>
/// All available overviews for the movie, if they should be included.
/// </summary>
public IReadOnlyList<Text> Overviews { get; set; } = [];
/// <summary>
/// Original language the movie was shot in.
/// </summary>
public string OriginalLanguage { get; set; } = string.Empty;
/// <summary>
/// Indicates the movie is restricted to an age group above the legal age,
/// because it's a pornography.
/// </summary>
public bool IsRestricted { get; set; }
/// <summary>
/// Indicates the entry is not truly a movie, including but not limited to
/// the types:
///
/// - official compilations,
/// - best of,
/// - filmed sport events,
/// - music concerts,
/// - plays or stand-up show,
/// - fitness video,
/// - health video,
/// - live movie theater events (art, music),
/// - and how-to DVDs,
///
/// among others.
/// </summary>
public bool IsVideo { get; set; }
/// <summary>
/// User rating of the movie from TMDB users.
/// </summary>
public Rating UserRating { get; set; } = new();
/// <summary>
/// The movie run-time, if it is known.
/// </summary>
public TimeSpan? Runtime { get; set; } = null;
/// <summary>
/// Genres.
/// </summary>
public IReadOnlyList<string> Genres { get; set; } = [];
/// <summary>
/// Keywords.
/// </summary>
public IReadOnlyList<string> Keywords { get; set; } = [];
/// <summary>
/// Content ratings for different countries for this movie.
/// </summary>
public IReadOnlyList<ContentRating> ContentRatings { get; set; } = [];
/// <summary>
/// The production countries.
/// </summary>
public IReadOnlyDictionary<string, string> ProductionCountries { get; set; } = new Dictionary<string, string>();
/// <summary>
/// The production companies (studios) that produced the movie.
/// </summary>
public IReadOnlyList<Studio> Studios { get; set; } = [];
/// <summary>
/// The cast that have worked on this movie.
/// </summary>
public IReadOnlyList<Role> Cast { get; set; } = [];
/// <summary>
/// The crew that have worked on this movie.
/// </summary>
public IReadOnlyList<Role> Crew { get; set; } = [];
/// <summary>
/// The yearly seasons this series belongs to.
/// </summary>
public List<YearlySeason> YearlySeasons { get; set; } = [];
/// <summary>
/// TMDB movie to file cross-references.
/// </summary>
public IReadOnlyList<CrossReference> FileCrossReferences { get; set; } = [];
/// <summary>
/// The date the movie first released, if it is known.
/// </summary>
public DateOnly? ReleasedAt { get; set; }
/// <summary>
/// When the local metadata was first created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the local metadata was last updated with new changes from the
/// remote.
/// </summary>
public DateTime LastUpdatedAt { get; set; }
string ITmdbEntity.Id => Id.ToString();
BaseItemKind ITmdbEntity.Kind => BaseItemKind.Movie;
public TmdbMovieInfo ToInfo() => new() {
TmdbMovieId = Id.ToString(),
TmdbMovieCollectionId = CollectionId?.ToString(),
};
}
================================================
FILE: Shokofin/API/Models/TMDB/TmdbMovieCollection.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
namespace Shokofin.API.Models.TMDB;
public class TmdbMovieCollection : ITmdbEntity {
/// <summary>
/// TMDB Movie Collection ID.
/// </summary>
[JsonPropertyName("ID")]
public int Id { get; init; }
/// <summary>
/// Preferred title based upon series title preference.
/// </summary>
public string Title { get; init; } = string.Empty;
/// <summary>
/// All available titles for the movie collection, if they should be included.
/// </summary>
public IReadOnlyList<Title> Titles { get; init; } = [];
/// <summary>
/// Preferred overview based upon description preference.
/// </summary>
public string Overview { get; init; } = string.Empty;
/// <summary>
/// All available overviews for the movie collection, if they should be included.
/// </summary>
public IReadOnlyList<Text> Overviews { get; init; } = [];
public int MovieCount { get; init; }
/// <summary>
/// When the local metadata was first created.
/// </summary>
public DateTime CreatedAt { get; init; }
/// <summary>
/// When the local metadata was last updated with new changes from the
/// remote.
/// </summary>
public DateTime LastUpdatedAt { get; init; }
string ITmdbEntity.Id => Id.ToString();
BaseItemKind ITmdbEntity.Kind => BaseItemKind.BoxSet;
}
================================================
FILE: Shokofin/API/Models/TMDB/TmdbMovieCrossReference.cs
================================================
using System.Text.Json.Serialization;
namespace
gitextract_n8jd1_7_/ ├── .config/ │ └── dotnet-tools.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ └── features.yml │ └── workflows/ │ ├── changelog.jq │ ├── git-log-json.mjs │ ├── release-daily.yml │ ├── release.yml │ └── release_draft.jq ├── .gitignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── LICENSE ├── README.md ├── Shokofin/ │ ├── API/ │ │ ├── Converters/ │ │ │ └── JsonAutoStringConverter.cs │ │ ├── IdPrefix.cs │ │ ├── Info/ │ │ │ ├── AniDB/ │ │ │ │ ├── AnidbAnimeInfo.cs │ │ │ │ └── AnidbEpisodeInfo.cs │ │ │ ├── CollectionInfo.cs │ │ │ ├── EpisodeInfo.cs │ │ │ ├── FileInfo.cs │ │ │ ├── IBaseItemInfo.cs │ │ │ ├── IExtendedItemInfo.cs │ │ │ ├── SeasonInfo.cs │ │ │ ├── Shoko/ │ │ │ │ ├── ShokoEpisodeInfo.cs │ │ │ │ └── ShokoSeriesInfo.cs │ │ │ ├── ShowInfo.cs │ │ │ └── TMDB/ │ │ │ ├── TmdbEpisodeInfo.cs │ │ │ ├── TmdbMovieInfo.cs │ │ │ ├── TmdbSeasonInfo.cs │ │ │ └── TmdbShowInfo.cs │ │ ├── Models/ │ │ │ ├── AniDB/ │ │ │ │ ├── AnidbAnime.cs │ │ │ │ └── AnidbEpisode.cs │ │ │ ├── ApiException.cs │ │ │ ├── ApiKey.cs │ │ │ ├── ComponentVersion.cs │ │ │ ├── ContentRating.cs │ │ │ ├── CrossReference.cs │ │ │ ├── EpisodeType.cs │ │ │ ├── File.cs │ │ │ ├── IDs.cs │ │ │ ├── Image.cs │ │ │ ├── Images.cs │ │ │ ├── ListResult.cs │ │ │ ├── ManagedFolder.cs │ │ │ ├── Rating.cs │ │ │ ├── Relation.cs │ │ │ ├── ReleaseGroup.cs │ │ │ ├── ReleaseInfo.cs │ │ │ ├── ReleaseSource.cs │ │ │ ├── Role.cs │ │ │ ├── SeriesType.cs │ │ │ ├── Shoko/ │ │ │ │ ├── ShokoEpisode.cs │ │ │ │ ├── ShokoGroup.cs │ │ │ │ └── ShokoSeries.cs │ │ │ ├── Studio.cs │ │ │ ├── TMDB/ │ │ │ │ ├── AlternateOrderingType.cs │ │ │ │ ├── ITmdbEntity.cs │ │ │ │ ├── ITmdbParentEntity.cs │ │ │ │ ├── TmdbEpisode.cs │ │ │ │ ├── TmdbEpisodeCrossReference.cs │ │ │ │ ├── TmdbMovie.cs │ │ │ │ ├── TmdbMovieCollection.cs │ │ │ │ ├── TmdbMovieCrossReference.cs │ │ │ │ ├── TmdbSeason.cs │ │ │ │ └── TmdbShow.cs │ │ │ ├── Tag.cs │ │ │ ├── Text.cs │ │ │ ├── Title.cs │ │ │ ├── TitleType.cs │ │ │ ├── YearlySeason.cs │ │ │ └── YearlySeasonName.cs │ │ ├── ShokoApiClient.cs │ │ ├── ShokoApiManager.cs │ │ └── ShokoIdLookup.cs │ ├── Collections/ │ │ └── CollectionManager.cs │ ├── Configuration/ │ │ ├── AllDescriptionsConfiguration.cs │ │ ├── AllImagesConfiguration.cs │ │ ├── AllTitlesConfiguration.cs │ │ ├── DebugConfiguration.cs │ │ ├── DescriptionConfiguration.cs │ │ ├── Enums/ │ │ │ ├── ImageLanguageType.cs │ │ │ ├── MetadataRefreshField.cs │ │ │ ├── SeasonMergingBehavior.cs │ │ │ ├── SeriesEpisodeConversion.cs │ │ │ ├── SeriesStructureType.cs │ │ │ └── VirtualRootLocation.cs │ │ ├── ImageConfiguration.cs │ │ ├── LegacyMediaFolderConfiguration.cs │ │ ├── LibraryConfiguration.cs │ │ ├── MediaFolderConfiguration.cs │ │ ├── MetadataRefreshConfiguration.cs │ │ ├── Models/ │ │ │ ├── LibraryConfigurationChangedEventArgs.cs │ │ │ └── MediaFolderConfigurationChangedEventArgs.cs │ │ ├── PluginConfiguration.cs │ │ ├── SeriesConfiguration.cs │ │ ├── Services/ │ │ │ ├── MediaFolderConfigurationService.cs │ │ │ └── SeriesConfigurationService.cs │ │ ├── TitleConfiguration.cs │ │ ├── TitlesConfiguration.cs │ │ └── UserConfiguration.cs │ ├── Events/ │ │ ├── EventDispatchService.cs │ │ ├── Interfaces/ │ │ │ ├── IFileEventArgs.cs │ │ │ ├── IFileRelocationEventArgs.cs │ │ │ ├── IMetadataUpdatedEventArgs.cs │ │ │ ├── IReleaseSavedEventArgs.cs │ │ │ ├── ProviderName.cs │ │ │ └── UpdateReason.cs │ │ ├── MetadataRefreshService.cs │ │ └── Stub/ │ │ └── FileEventArgsStub.cs │ ├── Extensions/ │ │ ├── CollectionTypeExtensions.cs │ │ ├── EnumerableExtensions.cs │ │ ├── EpisodeTypeExtensions.cs │ │ ├── ListExtensions.cs │ │ ├── MediaFolderConfigurationExtensions.cs │ │ ├── StringExtensions.cs │ │ └── SyncExtensions.cs │ ├── ExternalIds/ │ │ ├── AnidbAnimeId.cs │ │ ├── AnidbCreatorId.cs │ │ ├── AnidbEpisodeId.cs │ │ ├── ProviderNames.cs │ │ ├── ProviderUrls.cs │ │ ├── ShokoExternalUrlHandler.cs │ │ └── ShokoInternalId.cs │ ├── MergeVersions/ │ │ ├── MergeVersionManager.cs │ │ └── MergeVersionSortSelector.cs │ ├── Pages/ │ │ ├── Dummy.html │ │ ├── Scripts/ │ │ │ ├── Common.js │ │ │ ├── Dummy.js │ │ │ ├── Settings.js │ │ │ └── jsconfig.json │ │ └── Settings.html │ ├── Plugin.cs │ ├── PluginServiceRegistrator.cs │ ├── Providers/ │ │ ├── BoxSetProvider.cs │ │ ├── CustomBoxSetProvider.cs │ │ ├── CustomEpisodeProvider.cs │ │ ├── CustomMovieProvider.cs │ │ ├── CustomSeasonProvider.cs │ │ ├── CustomSeriesProvider.cs │ │ ├── EpisodeProvider.cs │ │ ├── ImageProvider.cs │ │ ├── MovieProvider.cs │ │ ├── SeasonProvider.cs │ │ ├── SeriesProvider.cs │ │ ├── TrailerProvider.cs │ │ └── VideoProvider.cs │ ├── Resolvers/ │ │ ├── Models/ │ │ │ ├── LinkGenerationResult.cs │ │ │ └── ShokoWatcher.cs │ │ ├── ShokoIgnoreRule.cs │ │ ├── ShokoLibraryMonitor.cs │ │ ├── ShokoResolver.cs │ │ └── VirtualFileSystemService.cs │ ├── Shokofin.csproj │ ├── SignalR/ │ │ ├── Models/ │ │ │ ├── EpisodeInfoUpdatedEventArgs.cs │ │ │ ├── FileEventArgs.cs │ │ │ ├── FileMovedEventArgs.cs │ │ │ ├── FileRenamedEventArgs.cs │ │ │ ├── MovieInfoUpdatedEventArgs.cs │ │ │ ├── ReleaseSavedEventArgs.cs │ │ │ └── SeriesInfoUpdatedEventArgs.cs │ │ ├── SignalRConnectionManager.cs │ │ ├── SignalREntryPoint.cs │ │ └── SignalRRetryPolicy.cs │ ├── Sync/ │ │ ├── SyncDirection.cs │ │ └── UserDataSyncManager.cs │ ├── Tasks/ │ │ ├── AutoRefreshMetadataTask.cs │ │ ├── CleanupVirtualRootTask.cs │ │ ├── ClearPluginCacheTask.cs │ │ ├── ExportUserDataTask.cs │ │ ├── ImportUserDataTask.cs │ │ ├── MergeEpisodesTask.cs │ │ ├── MergeMoviesTask.cs │ │ ├── PostScanTask.cs │ │ ├── ReconstructCollectionsTask.cs │ │ ├── SplitEpisodesTask.cs │ │ ├── SplitMoviesTask.cs │ │ ├── SyncUserDataTask.cs │ │ └── VersionCheckTask.cs │ ├── Utils/ │ │ ├── ContentRating.cs │ │ ├── DisposableAction.cs │ │ ├── GuardedMemoryCache.cs │ │ ├── IgnorePatterns.cs │ │ ├── ImageUtility.cs │ │ ├── LibraryScanWatcher.cs │ │ ├── Ordering.cs │ │ ├── PropertyWatcher.cs │ │ ├── SeriesInfoRelationComparer.cs │ │ ├── TagFilter.cs │ │ ├── TextUtility.cs │ │ └── UsageTracker.cs │ └── Web/ │ ├── ImageHostUrl.cs │ ├── Models/ │ │ ├── SimpleSeries.cs │ │ └── VfsLibraryPreview.cs │ ├── ShokofinHostController.cs │ ├── ShokofinSignalRController.cs │ ├── ShokofinUtilityController.cs │ └── VfsActionFilter.cs ├── Shokofin.sln ├── build.yaml ├── build_plugin.py └── manifest.json
SYMBOL INDEX (992 symbols across 181 files)
FILE: Shokofin/API/Converters/JsonAutoStringConverter.cs
class JsonAutoStringConverter (line 12) | public class JsonAutoStringConverter : JsonConverter<string> {
method CanConvert (line 13) | public override bool CanConvert(Type typeToConvert)
method Read (line 16) | public override string? Read(ref Utf8JsonReader reader, Type typeToCon...
method Write (line 32) | public override void Write(Utf8JsonWriter writer, string? value, JsonS...
FILE: Shokofin/API/IdPrefix.cs
type IdPrefix (line 4) | internal struct IdPrefix {
FILE: Shokofin/API/Info/AniDB/AnidbAnimeInfo.cs
class AnidbAnimeInfo (line 4) | public class AnidbAnimeInfo {
FILE: Shokofin/API/Info/AniDB/AnidbEpisodeInfo.cs
class AnidbEpisodeInfo (line 6) | public class AnidbEpisodeInfo {
method GetEpisodeNumberText (line 15) | public string GetEpisodeNumberText() => Type.ToShortString() + Episode...
FILE: Shokofin/API/Info/CollectionInfo.cs
class CollectionInfo (line 10) | public class CollectionInfo(ShokoGroup group, string? mainSeasonId, List...
method CollectionInfo (line 82) | public CollectionInfo(ShokoGroup group, ShokoSeries series, string? ma...
FILE: Shokofin/API/Info/EpisodeInfo.cs
class EpisodeInfo (line 23) | public class EpisodeInfo : IExtendedItemInfo {
method EpisodeInfo (line 120) | public EpisodeInfo(
method EpisodeInfo (line 275) | public EpisodeInfo(ShokoApiClient client, TmdbEpisode tmdbEpisode, Tmd...
method EpisodeInfo (line 329) | public EpisodeInfo(ShokoApiClient client, TmdbMovie tmdbMovie, ShokoEp...
method GetImages (line 385) | public async Task<EpisodeImages> GetImages(CancellationToken cancellat...
method GetImagePath (line 392) | private static string? GetImagePath(Image image)
method RoleToPersonInfo (line 395) | private static PersonInfo? RoleToPersonInfo(IReadOnlyList<Role> roles,...
FILE: Shokofin/API/Info/FileInfo.cs
class FileInfo (line 8) | public class FileInfo(File file, string seriesId, IReadOnlyList<(Episode...
FILE: Shokofin/API/Info/IBaseItemInfo.cs
type IBaseItemInfo (line 10) | public interface IBaseItemInfo {
FILE: Shokofin/API/Info/IExtendedItemInfo.cs
type IExtendedItemInfo (line 8) | public interface IExtendedItemInfo : IBaseItemInfo {
FILE: Shokofin/API/Info/SeasonInfo.cs
class SeasonInfo (line 23) | public class SeasonInfo : IExtendedItemInfo {
method SeasonInfo (line 204) | public SeasonInfo(
method SeasonInfo (line 459) | public SeasonInfo(ShokoApiClient client, TmdbSeason tmdbSeason, TmdbSh...
method SeasonInfo (line 530) | public SeasonInfo(ShokoApiClient client, TmdbMovie tmdbMovie, EpisodeI...
method SeasonInfo (line 580) | public SeasonInfo(ShokoApiClient client, TmdbMovieCollection tmdbMovie...
method AddYearlySeasons (line 638) | private void AddYearlySeasons(ref List<string> genres, ref List<string...
method GetFiles (line 654) | public async Task<IReadOnlyList<(File file, string seriesId, HashSet<s...
method GetImages (line 726) | public async Task<Images> GetImages(CancellationToken cancellationToken)
method IsExtraEpisode (line 734) | public bool IsExtraEpisode(EpisodeInfo? episodeInfo)
method IsEmpty (line 737) | public bool IsEmpty(int offset = 0) {
FILE: Shokofin/API/Info/Shoko/ShokoEpisodeInfo.cs
class ShokoEpisodeInfo (line 4) | public class ShokoEpisodeInfo {
FILE: Shokofin/API/Info/Shoko/ShokoSeriesInfo.cs
class ShokoSeriesInfo (line 4) | public class ShokoSeriesInfo {
FILE: Shokofin/API/Info/ShowInfo.cs
class ShowInfo (line 24) | public class ShowInfo : IExtendedItemInfo {
method ShowInfo (line 234) | public ShowInfo(ShokoApiClient client, SeasonInfo seasonInfo, TmdbShow...
method ShowInfo (line 287) | public ShowInfo(
method ShowInfo (line 413) | public ShowInfo(ShokoApiClient client, TmdbShow tmdbShow, IReadOnlyLis...
method ShowInfo (line 489) | public ShowInfo(ShokoApiClient client, TmdbMovie tmdbMovie, SeasonInfo...
method ShowInfo (line 527) | public ShowInfo(ShokoApiClient client, TmdbMovieCollection tmdbMovieCo...
method GetImages (line 609) | public async Task<Images> GetImages(CancellationToken cancellationToken)
method IsSpecial (line 619) | public bool IsSpecial(EpisodeInfo episodeInfo)
method TryGetBaseSeasonNumberForSeasonInfo (line 622) | public bool TryGetBaseSeasonNumberForSeasonInfo(SeasonInfo season, out...
method GetBaseSeasonNumberForSeasonInfo (line 625) | public int GetBaseSeasonNumberForSeasonInfo(SeasonInfo season)
method GetSeasonInfoBySeasonNumber (line 628) | public SeasonInfo? GetSeasonInfoBySeasonNumber(int seasonNumber)
FILE: Shokofin/API/Info/TMDB/TmdbEpisodeInfo.cs
class TmdbEpisodeInfo (line 6) | public class TmdbEpisodeInfo {
FILE: Shokofin/API/Info/TMDB/TmdbMovieInfo.cs
class TmdbMovieInfo (line 6) | public class TmdbMovieInfo : IComparable<TmdbMovieInfo>, IEquatable<Tmdb...
method CompareTo (line 11) | public int CompareTo(TmdbMovieInfo? other)
method Equals (line 14) | public bool Equals(TmdbMovieInfo? other)
method Equals (line 17) | public override bool Equals(object? obj)
method GetHashCode (line 20) | public override int GetHashCode()
FILE: Shokofin/API/Info/TMDB/TmdbSeasonInfo.cs
class TmdbSeasonInfo (line 4) | public class TmdbSeasonInfo {
method ToShowInfo (line 15) | public TmdbShowInfo ToShowInfo() => new() {
FILE: Shokofin/API/Info/TMDB/TmdbShowInfo.cs
class TmdbShowInfo (line 6) | public class TmdbShowInfo : IComparable<TmdbShowInfo>, IEquatable<TmdbSh...
method CompareTo (line 15) | public int CompareTo(TmdbShowInfo? other)
method Equals (line 18) | public bool Equals(TmdbShowInfo? other)
method Equals (line 21) | public override bool Equals(object? obj)
method GetHashCode (line 24) | public override int GetHashCode()
FILE: Shokofin/API/Models/AniDB/AnidbAnime.cs
class AnidbAnime (line 8) | public class AnidbAnime {
class AnidbAnimeWithDate (line 73) | public class AnidbAnimeWithDate : AnidbAnime {
FILE: Shokofin/API/Models/AniDB/AnidbEpisode.cs
class AnidbEpisode (line 8) | public class AnidbEpisode {
method ToInfo (line 33) | public AnidbEpisodeInfo ToInfo() => new() {
FILE: Shokofin/API/Models/ApiException.cs
class ApiException (line 12) | [Serializable]
type ValidationResponse (line 15) | private record ValidationResponse {
method ApiException (line 31) | public ApiException(HttpStatusCode statusCode, string source, string? ...
method ApiException (line 37) | protected ApiException(HttpStatusCode statusCode, RemoteApiException i...
method ApiException (line 44) | protected ApiException(HttpStatusCode statusCode, string source, strin...
method FromResponse (line 50) | public static ApiException FromResponse(HttpResponseMessage response) {
class RemoteApiException (line 69) | public class RemoteApiException : Exception {
method RemoteApiException (line 70) | public RemoteApiException(string source, string message, string stac...
type ApiExceptionType (line 79) | public enum ApiExceptionType {
FILE: Shokofin/API/Models/ApiKey.cs
class ApiKey (line 6) | public class ApiKey {
FILE: Shokofin/API/Models/ComponentVersion.cs
class ComponentVersionSet (line 9) | public class ComponentVersionSet {
class ComponentVersion (line 16) | public class ComponentVersion {
method ToString (line 38) | public override string ToString() {
type ReleaseChannel (line 51) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/ContentRating.cs
class ContentRating (line 5) | public class ContentRating : IEquatable<ContentRating> {
method Equals (line 26) | public bool Equals(ContentRating? other)
method Equals (line 36) | public override bool Equals(object? obj)
method GetHashCode (line 39) | public override int GetHashCode()
FILE: Shokofin/API/Models/CrossReference.cs
class CrossReference (line 7) | public class CrossReference {
class EpisodeCrossReferenceIDs (line 23) | public class EpisodeCrossReferenceIDs {
class CrossReferencePercentage (line 61) | public class CrossReferencePercentage {
class SeriesCrossReferenceIDs (line 87) | public class SeriesCrossReferenceIDs {
FILE: Shokofin/API/Models/EpisodeType.cs
type EpisodeType (line 5) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/File.cs
class File (line 7) | public class File {
class Location (line 74) | public class Location {
class UserStats (line 141) | public class UserStats {
FILE: Shokofin/API/Models/IDs.cs
class IDs (line 5) | public class IDs {
FILE: Shokofin/API/Models/Image.cs
class Image (line 6) | public class Image {
method Image (line 75) | public Image() { }
method Image (line 80) | public Image(Image image) : this() {
method ToURLString (line 102) | public string ToURLString(bool internalUrl = false)
type ImageSource (line 109) | [JsonConverter(typeof(JsonStringEnumConverter))]
type ShokoImageType (line 130) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/Images.cs
class Images (line 5) | public class Images {
class EpisodeImages (line 15) | public class EpisodeImages : Images {
FILE: Shokofin/API/Models/ListResult.cs
class ListResult (line 11) | public class ListResult<T> {
FILE: Shokofin/API/Models/ManagedFolder.cs
class ManagedFolder (line 5) | public class ManagedFolder {
FILE: Shokofin/API/Models/Rating.cs
class Rating (line 3) | public class Rating {
method Rating (line 32) | public Rating() { }
method Rating (line 37) | public Rating(Rating rating) {
method ToFloat (line 45) | public float ToFloat(int scale)
FILE: Shokofin/API/Models/Relation.cs
class Relation (line 8) | public class Relation {
class RelationIDs (line 32) | public class RelationIDs {
type RelationType (line 48) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/ReleaseGroup.cs
class ReleaseGroup (line 6) | public class ReleaseGroup {
FILE: Shokofin/API/Models/ReleaseInfo.cs
class ReleaseInfo (line 5) | public class ReleaseInfo {
FILE: Shokofin/API/Models/ReleaseSource.cs
type ReleaseSource (line 5) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/Role.cs
class Role (line 6) | public class Role : IEquatable<Role> {
method Equals (line 33) | public override bool Equals(object? obj)
method Equals (line 36) | public bool Equals(Role? other) {
method GetHashCode (line 47) | public override int GetHashCode()
class Person (line 50) | public class Person : IEquatable<Person> {
method Equals (line 86) | public override bool Equals(object? obj)
method Equals (line 89) | public bool Equals(Person? other) {
method GetHashCode (line 99) | public override int GetHashCode()
type CreatorRoleType (line 104) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/SeriesType.cs
type SeriesType (line 5) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/Shoko/ShokoEpisode.cs
class ShokoEpisode (line 9) | public class ShokoEpisode {
method ToInfo (line 69) | public ShokoEpisodeInfo ToInfo() => new() {
class EpisodeIDs (line 74) | public class EpisodeIDs : IDs {
class TmdbEpisodeIDs (line 88) | public class TmdbEpisodeIDs {
FILE: Shokofin/API/Models/Shoko/ShokoGroup.cs
class ShokoGroup (line 7) | public class ShokoGroup {
class GroupIDs (line 40) | public class GroupIDs : IDs {
class GroupSizes (line 51) | public class GroupSizes : ShokoSeries.SeriesSizes {
class SeriesTypeCounts (line 64) | public class SeriesTypeCounts {
FILE: Shokofin/API/Models/Shoko/ShokoSeries.cs
class ShokoSeries (line 8) | public class ShokoSeries {
class SeriesIDs (line 57) | public class SeriesIDs : IDs {
class TmdbSeriesIDs (line 89) | public class TmdbSeriesIDs {
class SeriesSizes (line 98) | public class SeriesSizes {
class EpisodeTypeCounts (line 146) | public class EpisodeTypeCounts {
class FileSourceCounts (line 157) | public class FileSourceCounts {
FILE: Shokofin/API/Models/Studio.cs
class Studio (line 9) | public class Studio {
FILE: Shokofin/API/Models/TMDB/AlternateOrderingType.cs
type AlternateOrderingType (line 4) | public enum AlternateOrderingType {
FILE: Shokofin/API/Models/TMDB/ITmdbEntity.cs
type ITmdbEntity (line 7) | public interface ITmdbEntity {
FILE: Shokofin/API/Models/TMDB/ITmdbParentEntity.cs
type ITmdbParentEntity (line 5) | public interface ITmdbParentEntity : ITmdbEntity {
FILE: Shokofin/API/Models/TMDB/TmdbEpisode.cs
class TmdbEpisode (line 13) | public class TmdbEpisode : ITmdbEntity {
method ToInfo (line 124) | public TmdbEpisodeInfo ToInfo() => new() {
class OrderingInformation (line 136) | public class OrderingInformation {
FILE: Shokofin/API/Models/TMDB/TmdbEpisodeCrossReference.cs
class TmdbEpisodeCrossReference (line 8) | public class TmdbEpisodeCrossReference {
FILE: Shokofin/API/Models/TMDB/TmdbMovie.cs
class TmdbMovie (line 9) | public class TmdbMovie : ITmdbParentEntity {
method ToInfo (line 152) | public TmdbMovieInfo ToInfo() => new() {
FILE: Shokofin/API/Models/TMDB/TmdbMovieCollection.cs
class TmdbMovieCollection (line 8) | public class TmdbMovieCollection : ITmdbEntity {
FILE: Shokofin/API/Models/TMDB/TmdbMovieCrossReference.cs
class TmdbMovieCrossReference (line 9) | public class TmdbMovieCrossReference {
FILE: Shokofin/API/Models/TMDB/TmdbSeason.cs
class TmdbSeason (line 9) | public class TmdbSeason : ITmdbEntity {
method ToInfo (line 85) | public TmdbSeasonInfo ToInfo() => new() {
FILE: Shokofin/API/Models/TMDB/TmdbShow.cs
class TmdbShow (line 9) | public class TmdbShow : ITmdbParentEntity {
method ToInfo (line 129) | public TmdbShowInfo ToInfo() => new() {
FILE: Shokofin/API/Models/Tag.cs
class Tag (line 10) | public class Tag {
class ResolvedTag (line 71) | public class ResolvedTag : Tag {
method ResolvedTag (line 192) | public ResolvedTag(Tag tag, ResolvedTag? parent, Func<string, int, IEn...
FILE: Shokofin/API/Models/Text.cs
class Text (line 5) | public class Text {
FILE: Shokofin/API/Models/Title.cs
class Title (line 5) | public class Title : Text {
FILE: Shokofin/API/Models/TitleType.cs
type TitleType (line 5) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/API/Models/YearlySeason.cs
class YearlySeason (line 6) | public class YearlySeason : IComparable<YearlySeason>, IEquatable<Yearly...
method CompareTo (line 19) | public int CompareTo(YearlySeason? other)
method Equals (line 28) | public bool Equals(YearlySeason? other)
method Equals (line 31) | public override bool Equals(object? obj)
method GetHashCode (line 34) | public override int GetHashCode()
FILE: Shokofin/API/Models/YearlySeasonName.cs
type YearlySeasonName (line 7) | public enum YearlySeasonName {
FILE: Shokofin/API/ShokoApiClient.cs
class ShokoApiClient (line 26) | public class ShokoApiClient : IDisposable {
method ShokoApiClient (line 53) | public ShokoApiClient(ILogger<ShokoApiClient> logger, UsageTracker tra...
method OnConfigurationChanged (line 80) | private void OnConfigurationChanged(object? sender, PluginConfiguratio...
method OnTrackerStalled (line 105) | private void OnTrackerStalled(object? sender, EventArgs eventArgs) {
method Clear (line 110) | public void Clear() {
method Dispose (line 115) | public void Dispose() {
method GetOrNull (line 123) | private async Task<ReturnType?> GetOrNull<ReturnType>(string url, stri...
method Get (line 132) | private Task<ReturnType> Get<ReturnType>(string url, string? apiKey = ...
method Get (line 135) | private async Task<ReturnType> Get<ReturnType>(string url, HttpMethod ...
method Get (line 163) | private async Task<HttpResponseMessage> Get(string url, HttpMethod met...
method Post (line 211) | private Task<ReturnType> Post<Type, ReturnType>(string url, Type body,...
method Post (line 214) | private async Task<ReturnType> Post<Type, ReturnType>(string url, Http...
method Post (line 243) | private async Task<HttpResponseMessage> Post<Type>(string url, HttpMet...
method GetApiKey (line 303) | public async Task<ApiKey?> GetApiKey(string username, string password,...
method GetVersion (line 332) | public async Task<ComponentVersion?> GetVersion() {
method CheckIfPluginsExposed (line 351) | public async Task<bool> CheckIfPluginsExposed(CancellationToken cancel...
method GetWebPrefix (line 354) | public async Task<string?> GetWebPrefix(CancellationToken cancellation...
method GetImageAsync (line 376) | public Task<HttpResponseMessage> GetImageAsync(ImageSource imageSource...
method GetManagedFolder (line 383) | public async Task<ManagedFolder?> GetManagedFolder(int managedFolderId)
method GetFilesInManagedFolder (line 388) | public async Task<ListResult<File>> GetFilesInManagedFolder(int manage...
method GetFile (line 397) | public async Task<File?> GetFile(string fileId)
method GetFileByEd2kAndFileSize (line 402) | public Task<File?> GetFileByEd2kAndFileSize(string ed2k, long fileSize)
method GetFileByPath (line 405) | public async Task<IReadOnlyList<File>> GetFileByPath(string relativePath)
method GetFileUserStats (line 412) | public async Task<File.UserStats?> GetFileUserStats(string fileId, str...
method PutFileUserStats (line 424) | public Task<File.UserStats> PutFileUserStats(string fileId, File.UserS...
method ScrobbleFile (line 427) | public async Task<bool> ScrobbleFile(string fileId, string episodeId, ...
method ScrobbleFile (line 431) | public async Task<bool> ScrobbleFile(string fileId, string episodeId, ...
method ScrobbleFile (line 435) | public async Task<bool> ScrobbleFile(string fileId, string episodeId, ...
method GetShokoEpisode (line 447) | public Task<ShokoEpisode?> GetShokoEpisode(string episodeId)
method GetShokoEpisodesInShokoSeries (line 460) | public Task<IReadOnlyList<ShokoEpisode>> GetShokoEpisodesInShokoSeries...
method GetShokoEpisodesForTmdbEpisode (line 491) | public async Task<IReadOnlyList<ShokoEpisode>> GetShokoEpisodesForTmdb...
method GetShokoEpisodesForTmdbMovie (line 494) | public async Task<IReadOnlyList<ShokoEpisode>> GetShokoEpisodesForTmdb...
method GetImagesForShokoEpisode (line 497) | public async Task<EpisodeImages?> GetImagesForShokoEpisode(string epis...
method GetShokoSeriesIdsForFilter (line 520) | public Task<IReadOnlyList<int>> GetShokoSeriesIdsForFilter(string filt...
method GetShokoSeries (line 523) | public Task<ShokoSeries?> GetShokoSeries(string seriesId)
method GetShokoSeriesForAnidbAnime (line 526) | public Task<ShokoSeries?> GetShokoSeriesForAnidbAnime(string animeId)
method GetShokoSeriesForShokoEpisode (line 529) | public Task<ShokoSeries?> GetShokoSeriesForShokoEpisode(string episodeId)
method GetShokoSeriesForDirectory (line 532) | public async Task<IReadOnlyList<ShokoSeries>> GetShokoSeriesForDirecto...
method GetShokoSeriesForTmdbMovie (line 535) | public async Task<IReadOnlyList<ShokoSeries>> GetShokoSeriesForTmdbMov...
method GetShokoSeriesForTmdbShow (line 538) | public async Task<IReadOnlyList<ShokoSeries>> GetShokoSeriesForTmdbSho...
method GetShokoSeriesInGroup (line 541) | public async Task<IReadOnlyList<ShokoSeries>> GetShokoSeriesInGroup(st...
method GetCastForShokoSeries (line 544) | public async Task<IReadOnlyList<Role>> GetCastForShokoSeries(string se...
method GetRelationsForShokoSeries (line 547) | public async Task<IReadOnlyList<Relation>> GetRelationsForShokoSeries(...
method GetTagsForShokoSeries (line 550) | public async Task<IReadOnlyList<Tag>> GetTagsForShokoSeries(string ser...
method GetImagesForShokoSeries (line 553) | public Task<Images?> GetImagesForShokoSeries(string seriesId, Cancella...
method GetFilesForShokoSeries (line 556) | public async Task<IReadOnlyList<File>> GetFilesForShokoSeries(string s...
method GetTmdbCrossReferencesForShokoSeries (line 561) | public async Task<IReadOnlyList<TmdbEpisodeCrossReference>> GetTmdbCro...
method GetShokoGroup (line 568) | public Task<ShokoGroup?> GetShokoGroup(string groupId)
method GetShokoGroupForShokoSeries (line 571) | public Task<ShokoGroup?> GetShokoGroupForShokoSeries(string seriesId)
method GetShokoGroupsInShokoGroup (line 574) | public async Task<IReadOnlyList<ShokoGroup>> GetShokoGroupsInShokoGrou...
method GetAllAnidbAnime (line 581) | public Task<ListResult<AnidbAnime>> GetAllAnidbAnime(string query = ""...
method GetTmdbEpisode (line 588) | public Task<TmdbEpisode?> GetTmdbEpisode(string episodeId, bool useDef...
method GetTmdbEpisodesInTmdbSeason (line 601) | public Task<IReadOnlyList<TmdbEpisode>> GetTmdbEpisodesInTmdbSeason(st...
method GetTmdbEpisodesInTmdbShow (line 632) | public Task<IReadOnlyList<TmdbEpisode>> GetTmdbEpisodesInTmdbShow(stri...
method GetImagesForTmdbEpisode (line 663) | public Task<EpisodeImages?> GetImagesForTmdbEpisode(string episodeId, ...
method GetTmdbSeasonForTmdbEpisode (line 670) | public Task<TmdbSeason?> GetTmdbSeasonForTmdbEpisode(string episodeId)
method GetTmdbSeason (line 673) | public Task<TmdbSeason?> GetTmdbSeason(string seasonId)
method GetTmdbSeasonsInTmdbShow (line 676) | public async Task<IReadOnlyList<TmdbSeason>> GetTmdbSeasonsInTmdbShow(...
method GetImagesForTmdbSeason (line 679) | public Task<Images?> GetImagesForTmdbSeason(string seasonId, Cancellat...
method GetFilesForTmdbSeason (line 682) | public async Task<IReadOnlyList<File>> GetFilesForTmdbSeason(string se...
method GetTmdbShowForSeason (line 691) | public Task<TmdbShow?> GetTmdbShowForSeason(string seasonId)
method GetImagesForTmdbShow (line 694) | public Task<Images?> GetImagesForTmdbShow(string showId, CancellationT...
method GetTmdbCrossReferencesForTmdbShow (line 697) | public async Task<IReadOnlyList<TmdbEpisodeCrossReference>> GetTmdbCro...
method GetTmdbMovie (line 704) | public Task<TmdbMovie?> GetTmdbMovie(string movieId)
method GetTmdbMoviesInMovieCollection (line 707) | public async Task<IReadOnlyList<TmdbMovie>> GetTmdbMoviesInMovieCollec...
method GetImagesForTmdbMovie (line 710) | public Task<EpisodeImages?> GetImagesForTmdbMovie(string movieId, Canc...
method GetFilesForTmdbMovie (line 713) | public async Task<IReadOnlyList<File>> GetFilesForTmdbMovie(string mov...
method GetTmdbCrossReferencesForTmdbMovie (line 718) | public async Task<IReadOnlyList<TmdbMovieCrossReference>> GetTmdbCross...
method GetTmdbMovieCollection (line 725) | public Task<TmdbMovieCollection?> GetTmdbMovieCollection(string collec...
method GetImagesForTmdbMovieCollection (line 728) | public Task<Images?> GetImagesForTmdbMovieCollection(string collection...
method GetCustomTags (line 739) | public async Task<IReadOnlyList<Tag>> GetCustomTags()
method GetSeriesIdsWithCustomTag (line 753) | public async Task<IReadOnlyList<int>> GetSeriesIdsWithCustomTag(IEnume...
method CreateCustomTag (line 762) | public Task<Tag> CreateCustomTag(string name, string? description = null)
method UpdateCustomTag (line 772) | public Task<Tag> UpdateCustomTag(int tagId, string? name = null, strin...
method RemoveCustomTag (line 780) | public async Task<bool> RemoveCustomTag(int tagId)
method GetCustomTagsForShokoSeries (line 790) | public async Task<IReadOnlyList<Tag>> GetCustomTagsForShokoSeries(int ...
method AddCustomTagToShokoSeries (line 799) | public async Task<bool> AddCustomTagToShokoSeries(int seriesId, int ta...
method RemoveCustomTagFromShokoSeries (line 808) | public async Task<bool> RemoveCustomTagFromShokoSeries(int seriesId, i...
FILE: Shokofin/API/ShokoApiManager.cs
class ShokoApiManager (line 29) | public partial class ShokoApiManager : IDisposable {
method YearRegex (line 31) | [System.Text.RegularExpressions.GeneratedRegex(@"\s+\((?<year>\d{4})(?...
method ShokoApiManager (line 60) | public ShokoApiManager(ILogger<ShokoApiManager> logger, ShokoApiClient...
method OnTrackerStalled (line 77) | private void OnTrackerStalled(object? sender, EventArgs eventArgs) {
method FindMediaFolder (line 96) | public (Folder mediaFolder, string partialPath) FindMediaFolder(string...
method StripMediaFolder (line 115) | public string StripMediaFolder(string fullPath) {
method Dispose (line 132) | public void Dispose() {
method Clear (line 137) | public void Clear() {
method GetInternalSeriesConfiguration (line 155) | internal Task<SeriesConfiguration> GetInternalSeriesConfiguration(stri...
method GetSeriesConfiguration (line 251) | private Task<SeriesConfiguration> GetSeriesConfiguration(string id)
method NormalizeCustomSeriesType (line 277) | private static string NormalizeCustomSeriesType(string seriesType) {
method GetNamespacedTagsForSeries (line 288) | public Task<IReadOnlyDictionary<string, ResolvedTag>> GetNamespacedTag...
method GetTagsForSeries (line 409) | private async Task<string[]> GetTagsForSeries(string seriesId) {
method GetGenresForSeries (line 414) | private async Task<string[]> GetGenresForSeries(string seriesId) {
method GetProductionLocations (line 419) | private async Task<string[]> GetProductionLocations(string seriesId) {
method GetAssumedContentRating (line 424) | private async Task<string?> GetAssumedContentRating(string seriesId) {
method GetPathSetForSeries (line 439) | public Task<HashSet<string>> GetPathSetForSeries(string seriesId)
method GetLocalEpisodeIdsForSeason (line 459) | public Task<HashSet<string>> GetLocalEpisodeIdsForSeason(SeasonInfo se...
method AddFileLookupIds (line 521) | internal void AddFileLookupIds(string path, string fileId, string seri...
method GetFileInfoByPath (line 526) | public async Task<(FileInfo?, SeasonInfo?, ShowInfo?)> GetFileInfoByPa...
method GetFileInfo (line 630) | public async Task<FileInfo?> GetFileInfo(string fileId, string seriesI...
method CreateFileInfo (line 646) | private Task<FileInfo> CreateFileInfo(File file, string fileId, string...
method TryGetFileAndSeriesIdForPath (line 716) | public bool TryGetFileAndSeriesIdForPath(string path, [NotNullWhen(tru...
method GetEpisodeInfo (line 753) | public async Task<EpisodeInfo?> GetEpisodeInfo(string episodeId) {
method CreateEpisodeInfo (line 784) | private Task<EpisodeInfo> CreateEpisodeInfo(TmdbMovie movie)
method CreateEpisodeInfo (line 801) | private Task<EpisodeInfo> CreateEpisodeInfo(TmdbEpisode episode, TmdbS...
method CreateEpisodeInfo (line 818) | private Task<EpisodeInfo> CreateEpisodeInfo(ShokoEpisode episode)
method GetExtraEpisodeDetailsForShokoSeries (line 865) | private Task<(IReadOnlyList<Role>, string[], string[], string[], strin...
method TryGetEpisodeIdsForPath (line 882) | public bool TryGetEpisodeIdsForPath(string path, [NotNullWhen(true)] o...
method TryGetEpisodeIdsForFileId (line 909) | public bool TryGetEpisodeIdsForFileId(string fileId, string seriesId, ...
method GetSeasonInfo (line 939) | public async Task<SeasonInfo?> GetSeasonInfo(string seasonId) {
method GetSeasonInfoByPath (line 976) | public async Task<SeasonInfo?> GetSeasonInfoByPath(string path) {
method GetSeasonInfoForEpisode (line 986) | public async Task<SeasonInfo?> GetSeasonInfoForEpisode(string episodeI...
method GetSeasonInfosForShokoSeries (line 1018) | public Task<IReadOnlyList<SeasonInfo>> GetSeasonInfosForShokoSeries(st...
method CreateSeasonInfo (line 1071) | private Task<SeasonInfo> CreateSeasonInfo(TmdbMovie tmdbMovie)
method CreateSeasonInfo (line 1089) | private Task<SeasonInfo> CreateSeasonInfo(TmdbMovieCollection tmdbMovi...
method CreateSeasonInfo (line 1109) | private Task<SeasonInfo> CreateSeasonInfo(TmdbSeason tmdbSeason, TmdbS...
method GetGroupIdsForAnidbAnime (line 1131) | private async Task<(string? topLevelShokoGroupId, AnidbAnimeInfo[] ani...
method CreateSeasonInfo (line 1177) | private async Task<SeasonInfo> CreateSeasonInfo(ShokoSeries series) {
method GetSeriesIdsForShokoSeries (line 1319) | public async Task<(string primaryId, List<string> extraIds)> GetSeries...
method GetSeriesIdsForSeason (line 1326) | private Task<(string primaryId, List<string> extraIds)> GetSeriesIdsFo...
method AdjustMainTitle (line 1580) | private string? AdjustMainTitle(string title)
method TryGetSeasonIdForPath (line 1589) | public bool TryGetSeasonIdForPath(string path, [NotNullWhen(true)] out...
method TryGetSeasonIdForEpisodeId (line 1615) | public bool TryGetSeasonIdForEpisodeId(string episodeId, [NotNullWhen(...
method SeasonNameRegex (line 1660) | [System.Text.RegularExpressions.GeneratedRegex(@"Season (?<seasonNumbe...
method GetSeasonIdForPath (line 1663) | private async Task<string?> GetSeasonIdForPath(string path) {
method GetShowInfoByPath (line 1736) | public async Task<ShowInfo?> GetShowInfoByPath(string path) {
method GetShowInfosForShokoSeries (line 1746) | public async Task<IReadOnlyList<ShowInfo>> GetShowInfosForShokoSeries(...
method GetShowInfoBySeasonId (line 1757) | public async Task<ShowInfo?> GetShowInfoBySeasonId(string seasonId) {
method CreateShowInfo (line 1812) | private Task<ShowInfo> CreateShowInfo(TmdbShow tmdbShow)
method CreateShowInfoForTmdbMovieCollection (line 1834) | private Task<ShowInfo> CreateShowInfoForTmdbMovieCollection(TmdbMovieC...
method CreateShowInfoForTmdbMovie (line 1854) | private Task<ShowInfo> CreateShowInfoForTmdbMovie(TmdbMovie tmdbMovie)
method CreateShowInfoForShokoGroup (line 1870) | private Task<ShowInfo?> CreateShowInfoForShokoGroup(ShokoGroup group, ...
method CreateShowInfoForShokoSeries (line 1939) | private Task<ShowInfo> CreateShowInfoForShokoSeries(SeasonInfo seasonI...
method TryGetShowIdForSeasonId (line 1962) | public bool TryGetShowIdForSeasonId(string seasonId, [NotNullWhen(true...
method GetCollectionInfo (line 1992) | public async Task<CollectionInfo?> GetCollectionInfo(string collection...
method CreateCollectionInfo (line 2007) | private Task<CollectionInfo> CreateCollectionInfo(ShokoGroup group, st...
FILE: Shokofin/API/ShokoIdLookup.cs
class ShokoIdLookup (line 19) | public class ShokoIdLookup(ShokoApiManager _apiManager, ILibraryManager ...
method IsEnabledForItem (line 29) | public bool IsEnabledForItem(BaseItem item) {
method IsEnabledForLibraryOptions (line 54) | internal static bool IsEnabledForLibraryOptions(LibraryOptions library...
method TryGetSeasonIdFor (line 78) | public bool TryGetSeasonIdFor(Series series, [NotNullWhen(true)] out s...
method TryGetSeasonIdFor (line 103) | public bool TryGetSeasonIdFor(Season season, [NotNullWhen(true)] out s...
method TryGetEpisodeIdsFor (line 124) | public bool TryGetEpisodeIdsFor(BaseItem item, [NotNullWhen(true)] out...
method TryGetFileAndSeriesIdFor (line 158) | public bool TryGetFileAndSeriesIdFor(BaseItem video, [NotNullWhen(true...
FILE: Shokofin/Collections/CollectionManager.cs
class CollectionManager (line 21) | public class CollectionManager(
method GetCollectionsFolder (line 30) | public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
method ReconstructCollections (line 33) | public async Task ReconstructCollections(IProgress<double> progress, C...
method ReconstructMovieSeriesCollections (line 56) | private async Task ReconstructMovieSeriesCollections(IProgress<double>...
method ReconstructSharedCollections (line 242) | private async Task ReconstructSharedCollections(IProgress<double> prog...
method CleanupAll (line 523) | private async Task CleanupAll(IProgress<double> progress, Cancellation...
method CleanupMovies (line 538) | private async Task CleanupMovies() {
method CleanupSeriesCollections (line 554) | private void CleanupSeriesCollections() {
method CleanupGroupCollections (line 570) | private void CleanupGroupCollections() {
method RemoveCollection (line 586) | private void RemoveCollection(BoxSet collection, string? seasonId = nu...
method GetMovies (line 598) | private List<Movie> GetMovies()
method GetShows (line 610) | private List<Series> GetShows()
method GetSeriesCollections (line 622) | private Dictionary<string, IReadOnlyList<BoxSet>> GetSeriesCollections()
method GetGroupCollections (line 635) | private Dictionary<string, IReadOnlyList<BoxSet>> GetGroupCollections()
FILE: Shokofin/Configuration/AllDescriptionsConfiguration.cs
class AllDescriptionsConfiguration (line 10) | public class AllDescriptionsConfiguration {
FILE: Shokofin/Configuration/AllImagesConfiguration.cs
class AllImagesConfiguration (line 8) | public class AllImagesConfiguration {
FILE: Shokofin/Configuration/AllTitlesConfiguration.cs
class AllTitlesConfiguration (line 9) | public class AllTitlesConfiguration {
FILE: Shokofin/Configuration/DebugConfiguration.cs
class DebugConfiguration (line 8) | public class DebugConfiguration {
FILE: Shokofin/Configuration/DescriptionConfiguration.cs
class DescriptionConfiguration (line 8) | public class DescriptionConfiguration {
method GetOrderedDescriptionProviders (line 32) | public IEnumerable<DescriptionProvider> GetOrderedDescriptionProviders()
class ToggleDescriptionConfiguration (line 36) | public class ToggleDescriptionConfiguration : DescriptionConfiguration {
FILE: Shokofin/Configuration/Enums/ImageLanguageType.cs
type ImageLanguageType (line 7) | public enum ImageLanguageType {
FILE: Shokofin/Configuration/Enums/MetadataRefreshField.cs
type MetadataRefreshField (line 8) | [Flags]
FILE: Shokofin/Configuration/Enums/SeasonMergingBehavior.cs
type SeasonMergingBehavior (line 8) | [Flags]
FILE: Shokofin/Configuration/Enums/SeriesEpisodeConversion.cs
type SeriesEpisodeConversion (line 7) | public enum SeriesEpisodeConversion {
FILE: Shokofin/Configuration/Enums/SeriesStructureType.cs
type SeriesStructureType (line 7) | public enum SeriesStructureType {
FILE: Shokofin/Configuration/Enums/VirtualRootLocation.cs
type VirtualRootLocation (line 7) | public enum VirtualRootLocation {
FILE: Shokofin/Configuration/ImageConfiguration.cs
class ImageConfiguration (line 6) | public class ImageConfiguration {
method GetOrderedPosterTypes (line 73) | public IReadOnlyList<ImageLanguageType> GetOrderedPosterTypes()
method GetOrderedLogoTypes (line 79) | public IReadOnlyList<ImageLanguageType> GetOrderedLogoTypes()
method GetOrderedBackdropTypes (line 85) | public IReadOnlyList<ImageLanguageType> GetOrderedBackdropTypes()
class ToggleImageConfiguration (line 89) | public class ToggleImageConfiguration : ImageConfiguration {
FILE: Shokofin/Configuration/LegacyMediaFolderConfiguration.cs
class LegacyMediaFolderConfiguration (line 14) | [XmlType("MediaFolderConfiguration")]
FILE: Shokofin/Configuration/LibraryConfiguration.cs
class LibraryConfiguration (line 16) | public class LibraryConfiguration {
FILE: Shokofin/Configuration/MediaFolderConfiguration.cs
class MediaFolderConfiguration (line 11) | [XmlType("MediaFolderConfiguration_V2")]
method IsEnabledForPath (line 70) | public bool IsEnabledForPath(string relativePath)
method MergeWith (line 77) | public void MergeWith(MediaFolderConfiguration other) {
FILE: Shokofin/Configuration/MetadataRefreshConfiguration.cs
class MetadataRefreshConfiguration (line 9) | public class MetadataRefreshConfiguration {
FILE: Shokofin/Configuration/Models/LibraryConfigurationChangedEventArgs.cs
class LibraryConfigurationChangedEventArgs (line 6) | public class LibraryConfigurationChangedEventArgs(LibraryConfiguration l...
FILE: Shokofin/Configuration/Models/MediaFolderConfigurationChangedEventArgs.cs
class MediaConfigurationChangedEventArgs (line 5) | public class MediaConfigurationChangedEventArgs(LibraryConfiguration lib...
FILE: Shokofin/Configuration/PluginConfiguration.cs
class PluginConfiguration (line 26) | public class PluginConfiguration : BasePluginConfiguration {
method PluginConfiguration (line 791) | public PluginConfiguration() {
FILE: Shokofin/Configuration/SeriesConfiguration.cs
class SeriesConfiguration (line 10) | public class SeriesConfiguration {
class NullableSeriesConfiguration (line 51) | public class NullableSeriesConfiguration {
FILE: Shokofin/Configuration/Services/MediaFolderConfigurationService.cs
class MediaFolderConfigurationService (line 21) | public class MediaFolderConfigurationService {
method MediaFolderConfigurationService (line 56) | public MediaFolderConfigurationService(
method OnLibraryScanValueChanged (line 92) | private void OnLibraryScanValueChanged(object? sender, bool isRunning) {
method OnUsageTrackerStalled (line 99) | private void OnUsageTrackerStalled(object? sender, EventArgs eventArgs) {
method EditLibraries (line 103) | private async Task EditLibraries(bool shouldScheduleLibraryScan) {
method ConstructKey (line 137) | private int ConstructKey(LibraryConfiguration config)
method OnConfigurationChanged (line 140) | private void OnConfigurationChanged(object? sender, PluginConfiguratio...
method OnLibraryManagerItemRemoved (line 150) | private async void OnLibraryManagerItemRemoved(object? sender, ItemCha...
method GetAvailableMediaFoldersForLibraries (line 168) | public async Task<IReadOnlyList<(string vfsPath, CollectionType? colle...
method GetMediaFoldersForLibraryInVFS (line 199) | public async Task<(LibraryConfiguration? vfsRootConfig, IReadOnlyList<...
method GetOrCreateConfigurationForMediaFolder (line 216) | public async Task<(LibraryConfiguration? libraryConfiguration, MediaFo...
method GenerateAllConfigurations (line 245) | private async Task GenerateAllConfigurations(List<VirtualFolderInfo> a...
method AddToLibrary (line 418) | private void AddToLibrary(LibraryConfiguration config, string path) {
method RemoveFromLibrary (line 424) | private void RemoveFromLibrary(LibraryConfiguration config, string pat...
method CreateConfigurationForPath (line 430) | private async Task<MediaFolderConfiguration> CreateConfigurationForPat...
method GetSamplePaths (line 517) | private IEnumerable<string> GetSamplePaths(string mediaFolder) {
method GetVirtualFolders (line 560) | private List<VirtualFolderInfo> GetVirtualFolders()
FILE: Shokofin/Configuration/Services/SeriesConfigurationService.cs
class SeriesConfigurationService (line 14) | public class SeriesConfigurationService(ILogger<SeriesConfigurationServi...
method CreatOrGetRequiredTags (line 196) | private Task<IReadOnlyDictionary<string, int>> CreatOrGetRequiredTags()
method GetSeriesConfigurationForId (line 236) | public async Task<SeriesConfiguration?> GetSeriesConfigurationForId(in...
method UpdateSeriesConfigurationForId (line 243) | public async Task<SeriesConfiguration> UpdateSeriesConfigurationForId(...
method UpdateSeriesConfigurationForId (line 265) | public async Task<SeriesConfiguration> UpdateSeriesConfigurationForId(...
class SimpleTag (line 465) | class SimpleTag {
FILE: Shokofin/Configuration/TitleConfiguration.cs
class TitleConfiguration (line 8) | public class TitleConfiguration {
method GetOrderedTitleProviders (line 36) | public IEnumerable<TitleProvider> GetOrderedTitleProviders()
FILE: Shokofin/Configuration/TitlesConfiguration.cs
class TitlesConfiguration (line 8) | public class TitlesConfiguration {
class ToggleTitlesConfiguration (line 27) | public class ToggleTitlesConfiguration : TitlesConfiguration {
FILE: Shokofin/Configuration/UserConfiguration.cs
class UserConfiguration (line 9) | public class UserConfiguration {
FILE: Shokofin/Events/EventDispatchService.cs
class EventDispatchService (line 32) | public class EventDispatchService {
method EventDispatchService (line 72) | public EventDispatchService(
method OnStalled (line 106) | private void OnStalled(object? sender, EventArgs eventArgs) {
method Clear (line 110) | public void Clear() => RecentlyUpdatedEntitiesDict.Clear();
method RegisterEventSubmitter (line 114) | public IDisposable RegisterEventSubmitter() {
method DeregisterEventSubmitter (line 122) | private void DeregisterEventSubmitter() {
method OnIntervalElapsed (line 133) | private void OnIntervalElapsed(object? sender, ElapsedEventArgs eventA...
method ClearFileEvents (line 166) | private void ClearFileEvents() {
method ClearMetadataUpdatedEvents (line 178) | private void ClearMetadataUpdatedEvents() {
method AddFileEvent (line 194) | public void AddFileEvent(int fileId, UpdateReason reason, int managedF...
method ProcessFileEvents (line 204) | private async Task ProcessFileEvents(int fileId, List<(UpdateReason Re...
method GetSeriesIdsForFile (line 371) | private async Task<IReadOnlySet<string>> GetSeriesIdsForFile(int fileI...
method GetNewSourceLocation (line 414) | private async Task<string?> GetNewSourceLocation(int managedFolderId, ...
method RemoveSymbolicLink (line 433) | private void RemoveSymbolicLink(string filePath) {
method AddSeriesEvent (line 465) | public void AddSeriesEvent(string metadataId, IMetadataUpdatedEventArg...
method ProcessMetadataEvents (line 475) | private async Task ProcessMetadataEvents(string metadataId, List<IMeta...
method ProcessImageUpdateEvents (line 489) | private async Task ProcessImageUpdateEvents(string metadataId, List<IM...
method ProcessMetadataUpdateEvents (line 538) | private async Task ProcessMetadataUpdateEvents(string metadataId, List...
method ProcessSeriesEvents (line 592) | private async Task<int> ProcessSeriesEvents(ShowInfo showInfo, List<IM...
method ProcessMovieEvents (line 705) | private async Task<int> ProcessMovieEvents(SeasonInfo seasonInfo, List...
FILE: Shokofin/Events/Interfaces/IFileEventArgs.cs
type IFileEventArgs (line 6) | public interface IFileEventArgs {
class FileCrossReference (line 41) | public class FileCrossReference {
FILE: Shokofin/Events/Interfaces/IFileRelocationEventArgs.cs
type IFileRelocationEventArgs (line 4) | public interface IFileRelocationEventArgs : IFileEventArgs {
FILE: Shokofin/Events/Interfaces/IMetadataUpdatedEventArgs.cs
type IMetadataUpdatedEventArgs (line 7) | public interface IMetadataUpdatedEventArgs {
FILE: Shokofin/Events/Interfaces/IReleaseSavedEventArgs.cs
type IReleaseSavedEventArgs (line 4) | public interface IReleaseSavedEventArgs {
FILE: Shokofin/Events/Interfaces/ProviderName.cs
type ProviderName (line 5) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/Events/Interfaces/UpdateReason.cs
type UpdateReason (line 5) | [JsonConverter(typeof(JsonStringEnumConverter))]
FILE: Shokofin/Events/MetadataRefreshService.cs
class MetadataRefreshService (line 24) | public class MetadataRefreshService {
method MetadataRefreshService (line 59) | public MetadataRefreshService(
method RefreshCollection (line 73) | public async Task<bool> RefreshCollection(BoxSet boxSet, MetadataRefre...
method RefreshMovie (line 104) | public async Task<bool> RefreshMovie(Movie movie, MetadataRefreshField...
method RefreshSeries (line 150) | public async Task<bool> RefreshSeries(Series series, MetadataRefreshFi...
method RefreshSeason (line 185) | public async Task<bool> RefreshSeason(Season season, MetadataRefreshFi...
method RefreshEpisode (line 225) | public async Task<bool> RefreshEpisode(Episode episode, MetadataRefres...
method RefreshVideo (line 282) | public async Task<bool> RefreshVideo(Video video, MetadataRefreshField...
method RefreshInternal (line 316) | private async Task<bool> RefreshInternal(BaseItem item, MetadataRefres...
method RefreshBaseItem (line 331) | private async Task<bool> RefreshBaseItem<T>(
method LegacyRefreshMetadata (line 484) | private async Task<bool> LegacyRefreshMetadata(BaseItem item, bool upd...
method LegacyRefreshImages (line 499) | private async Task<bool> LegacyRefreshImages(BaseItem item, bool recur...
method AutoRefresh (line 514) | public async Task AutoRefresh(IProgress<double>? progress = null, Canc...
method GetMovies (line 540) | private List<Movie> GetMovies(MetadataRefreshConfiguration config)
method GetEpisodes (line 552) | private List<Episode> GetEpisodes(MetadataRefreshConfiguration config)
method FilterBaseItem (line 564) | private Func<BaseItem, bool> FilterBaseItem(MetadataRefreshConfigurati...
FILE: Shokofin/Events/Stub/FileEventArgsStub.cs
class FileEventArgsStub (line 8) | public class FileEventArgsStub : IFileEventArgs {
method FileEventArgsStub (line 27) | public FileEventArgsStub(int fileId, int? fileLocationId, int managedF...
method FileEventArgsStub (line 35) | public FileEventArgsStub(File.Location location, File file) {
FILE: Shokofin/Extensions/CollectionTypeExtensions.cs
class CollectionTypeExtensions (line 7) | public static class CollectionTypeExtensions {
method ConvertToCollectionType (line 8) | public static CollectionType? ConvertToCollectionType(this CollectionT...
FILE: Shokofin/Extensions/EnumerableExtensions.cs
class EnumerableExtensions (line 7) | public static class EnumerableExtensions {
method WhereNotNull (line 8) | [return: NotNullIfNotNull(nameof(enumerable))]
method WhereNotNull (line 12) | [return: NotNullIfNotNull(nameof(enumerable))]
method WhereNotNullOrDefault (line 16) | [return: NotNullIfNotNull(nameof(enumerable))]
method WhereNotNullOrDefault (line 20) | [return: NotNullIfNotNull(nameof(enumerable))]
FILE: Shokofin/Extensions/EpisodeTypeExtensions.cs
class EpisodeTypeExtensions (line 6) | public static class EpisodeTypeExtensions {
method ToShortString (line 7) | public static string ToShortString(this EpisodeType episodeType)
FILE: Shokofin/Extensions/ListExtensions.cs
class ListExtensions (line 6) | public static class ListExtensions {
method TryRemoveAt (line 7) | public static bool TryRemoveAt<T>(this List<T> list, int index, [NotNu...
method GetRange (line 17) | public static IEnumerable<T> GetRange<T>(this IReadOnlyList<T> list, i...
FILE: Shokofin/Extensions/MediaFolderConfigurationExtensions.cs
class MediaFolderConfigurationExtensions (line 9) | public static class MediaFolderConfigurationExtensions {
method GetFolderForPath (line 10) | public static Folder GetFolderForPath(this string mediaFolderPath)
method ToManagedFolderList (line 14) | public static IReadOnlyList<(int managedFolderId, string managedFolder...
method ToManagedFolderList (line 20) | public static IReadOnlyList<(string managedFolderSubPath, bool vfsEnab...
FILE: Shokofin/Extensions/StringExtensions.cs
class StringExtensions (line 21) | public static partial class StringExtensions {
method Replace (line 22) | public static string Replace(this string input, Regex regex, string re...
method Replace (line 25) | public static string Replace(this string input, Regex regex, MatchEval...
method Replace (line 28) | public static string Replace(this string input, Regex regex, MatchEval...
method Replace (line 31) | public static string Replace(this string input, Regex regex, MatchEval...
method Replace (line 34) | public static string Replace(this string input, Regex regex, string re...
method Replace (line 37) | public static string Replace(this string input, Regex regex, string re...
method Deconstruct (line 40) | public static void Deconstruct(this IList<string> list, out string fir...
method Deconstruct (line 44) | public static void Deconstruct(this IList<string> list, out string fir...
method Deconstruct (line 49) | public static void Deconstruct(this IList<string> list, out string fir...
method Deconstruct (line 55) | public static void Deconstruct(this IList<string> list, out string fir...
method Deconstruct (line 62) | public static void Deconstruct(this IList<string> list, out string fir...
method Join (line 70) | public static string Join(this IEnumerable<string> list, char separator)
method Join (line 73) | public static string Join(this IEnumerable<string> list, string? separ...
method Join (line 76) | public static string Join(this IEnumerable<string> list, char separato...
method Join (line 79) | public static string Join(this IEnumerable<string> list, string? separ...
method Join (line 82) | public static string Join(this IEnumerable<char> list, char separator)
method Join (line 85) | public static string Join(this IEnumerable<char> list, string? separator)
method Join (line 88) | public static string Join(this IEnumerable<char> list, char separator,...
method Join (line 91) | public static string Join(this IEnumerable<char> list, string? separat...
method IsAllowedCharacter (line 94) | private static char? IsAllowedCharacter(this char c)
method ForceASCII (line 97) | public static string ForceASCII(this string value)
method CompactUnderscore (line 100) | private static string CompactUnderscore(this string path)
method CompactWhitespaces (line 103) | public static string CompactWhitespaces(this string path)
method ReplaceInvalidPathCharacters (line 106) | public static string ReplaceInvalidPathCharacters(this string path)
method GetAttributeValue (line 120) | public static string? GetAttributeValue(this string text, string attri...
method GetPartRegex (line 158) | [GeneratedRegex(@"\.pt(?<partNumber>\d+)(?:\.[a-z0-9]+)?$", RegexOptio...
method TryGetAttributeValue (line 161) | public static bool TryGetAttributeValue(this string text, string attri...
method TryGetSeasonId (line 174) | public static bool TryGetSeasonId(this IHasProviderIds providerIds, [N...
method TryGetSeasonId (line 212) | public static bool TryGetSeasonId(this SeasonInfo seasonInfo, [NotNull...
method TryGetSeasonIdFromInternalId (line 221) | public static bool TryGetSeasonIdFromInternalId(this string internalId...
method TryGetEpisodeId (line 243) | public static bool TryGetEpisodeId(this IHasProviderIds providerIds, [...
method TryGetEpisodeIds (line 265) | public static bool TryGetEpisodeIds(this IHasProviderIds providerIds, ...
method TryGetFileAndSeriesId (line 288) | public static bool TryGetFileAndSeriesId(this IHasProviderIds provider...
FILE: Shokofin/Extensions/SyncExtensions.cs
class SyncExtensions (line 8) | public static class SyncExtensions {
method ToFileUserStats (line 9) | public static File.UserStats ToFileUserStats(this UserItemData userDat...
method CopyFrom (line 22) | public static bool CopyFrom(this UserItemData userData, UserItemData o...
method MergeWithFileUserStats (line 73) | public static UserItemData MergeWithFileUserStats(this UserItemData us...
method ToUserData (line 81) | public static UserItemData ToUserData(this File.UserStats userStats, V...
FILE: Shokofin/ExternalIds/AnidbAnimeId.cs
class AnidbAnimeId (line 8) | public class AnidbAnimeId : IExternalId {
method Supports (line 17) | public bool Supports(IHasProviderIds item) => item is Series or Season;
FILE: Shokofin/ExternalIds/AnidbCreatorId.cs
class AnidbCreatorId (line 8) | public class AnidbCreatorId : IExternalId {
method Supports (line 17) | public bool Supports(IHasProviderIds item) => item is Person;
FILE: Shokofin/ExternalIds/AnidbEpisodeId.cs
class AnidbEpisodeId (line 8) | public class AnidbEpisodeId : IExternalId {
method Supports (line 17) | public bool Supports(IHasProviderIds item) => item is Episode;
FILE: Shokofin/ExternalIds/ProviderNames.cs
type ProviderNames (line 4) | public struct ProviderNames {
FILE: Shokofin/ExternalIds/ProviderUrls.cs
type ProviderUrls (line 4) | public struct ProviderUrls {
FILE: Shokofin/ExternalIds/ShokoExternalUrlHandler.cs
class ShokoExternalUrlHandler (line 16) | public class ShokoExternalUrlHandler(ShokoIdLookup lookup) : IExternalUr...
method GetExternalUrls (line 27) | IEnumerable<string> IExternalUrlProvider.GetExternalUrls(BaseItem item)
method GetExternalUrls (line 46) | private static IReadOnlyCollection<(string, string)> GetExternalUrls(B...
method GetCollectionUrls (line 56) | private static IEnumerable<(string Name, string Url)> GetCollectionUrl...
method GetPersonUrls (line 69) | private static IEnumerable<(string Name, string Url)> GetPersonUrls(Pe...
method Deflate (line 79) | private static byte[] Deflate(byte[] data)
method Inflate (line 87) | private static byte[] Inflate(byte[] compressed)
method DeflateInfoUrls (line 96) | private static string DeflateInfoUrls(IEnumerable<(string ProviderName...
method InflateInfoUrls (line 99) | private static IEnumerable<(string Name, string Url)> InflateInfoUrls(...
method GetShowInfoUrls (line 134) | public static string GetShowInfoUrls(API.Info.ShowInfo showInfo) {
method GetSeasonInfoUrls (line 140) | public static string GetSeasonInfoUrls(API.Info.SeasonInfo seasonInfo) {
method GetEpisodeInfoUrls (line 146) | public static string GetEpisodeInfoUrls(API.Info.EpisodeInfo episodeIn...
method GetFileInfoUrls (line 152) | public static string GetFileInfoUrls(API.Info.FileInfo fileInfo) {
method AddShowInfoUrls (line 170) | private static void AddShowInfoUrls(ref List<(string ProviderName, str...
method AddSeasonInfoUrls (line 221) | private static void AddSeasonInfoUrls(ref List<(string ProviderName, s...
method AddEpisodeInfoUrls (line 266) | private static void AddEpisodeInfoUrls(ref List<(string ProviderName, ...
FILE: Shokofin/ExternalIds/ShokoInternalId.cs
class ShokoInternalId (line 10) | public class ShokoInternalId : IExternalId {
method Supports (line 27) | bool IExternalId.Supports(IHasProviderIds item) => item is BoxSet or S...
FILE: Shokofin/MergeVersions/MergeVersionManager.cs
class MergeVersionsManager (line 35) | public class MergeVersionsManager {
method MergeVersionsManager (line 67) | public MergeVersionsManager(ILogger<MergeVersionsManager> logger, ILib...
method OnUsageTrackerStalled (line 80) | private void OnUsageTrackerStalled(object? sender, EventArgs e) {
method Clear (line 84) | public void Clear() {
method SplitAndMergeAllEpisodes (line 105) | public async Task SplitAndMergeAllEpisodes(IProgress<double>? progress...
method SplitAllEpisodes (line 125) | public async Task SplitAllEpisodes(IProgress<double>? progress, Cancel...
method SplitAndMergeQueuedEpisodes (line 145) | private async Task SplitAndMergeQueuedEpisodes() {
method ScheduleSplitAndMergeEpisodesByEpisodeId (line 173) | public void ScheduleSplitAndMergeEpisodesByEpisodeId(string episodeId)
method SplitAndMergeAllMovies (line 193) | public async Task SplitAndMergeAllMovies(IProgress<double>? progress, ...
method SplitAllMovies (line 213) | public async Task SplitAllMovies(IProgress<double>? progress, Cancella...
method SplitAndMergeQueuedMovies (line 233) | private async Task SplitAndMergeQueuedMovies() {
method ScheduleSplitAndMergeMoviesByEpisodeId (line 261) | public void ScheduleSplitAndMergeMoviesByEpisodeId(string movieId)
method GetMoviesFromLibrary (line 273) | public IReadOnlyList<Movie> GetMoviesFromLibrary(string episodeId = "")
method GetEpisodesFromLibrary (line 291) | public IReadOnlyList<Episode> GetEpisodesFromLibrary(string episodeId ...
method SplitAndMergeVideos (line 311) | public async Task<bool> SplitAndMergeVideos<TVideo>(
method SplitVideos (line 363) | public async Task SplitVideos<TVideo>(IReadOnlyList<TVideo> videos, IP...
method MergeVideos (line 388) | private async Task MergeVideos<TVideo>(IEnumerable<TVideo> input) wher...
method CleanVideo (line 476) | private async Task CleanVideo<TVideo>(TVideo? video, HashSet<Guid> vis...
method GetOrderedSelectors (line 531) | private static MergeVersionSortSelector[] GetOrderedSelectors()
method OrderVideos (line 534) | private async Task<IList<(TVideo video, string? sortName)>> OrderVideo...
method GetSortName (line 543) | private async Task<string?> GetSortName<TVideo>(TVideo video, IList<Me...
method GetSelectedSortValue (line 556) | private string GetSelectedSortValue<TVideo>(TVideo video, FileInfo fil...
class LinkedChildComparer (line 593) | internal class LinkedChildComparer : IEqualityComparer<LinkedChild>
method Equals (line 599) | public bool Equals(LinkedChild? x, LinkedChild? y)
method GetHashCode (line 602) | public int GetHashCode([DisallowNull] LinkedChild obj)
FILE: Shokofin/MergeVersions/MergeVersionSortSelector.cs
type MergeVersionSortSelector (line 7) | public enum MergeVersionSortSelector {
FILE: Shokofin/Pages/Scripts/Common.js
method getConfiguration (line 594) | getConfiguration() {
method updateConfiguration (line 605) | updateConfiguration(config) {
method getApiKey (line 619) | getApiKey(username, password, userKey = false) {
method getSeriesList (line 643) | getSeriesList(query = "") {
method getSeriesConfiguration (line 660) | getSeriesConfiguration(seriesId) {
method updateSeriesConfiguration (line 676) | updateSeriesConfiguration(seriesId, partialSeriesConfiguration = { }) {
method getSignalrStatus (line 695) | getSignalrStatus() {
method signalrConnect (line 709) | async signalrConnect() {
method signalrDisconnect (line 723) | async signalrDisconnect() {
function updateTabs (line 871) | function updateTabs(view, tabName) {
function setupEvents (line 1046) | function setupEvents(view, events, initialTab = "connection", hide = fal...
function createControllerFactory (line 1221) | function createControllerFactory(options) {
function handleError (line 1239) | function handleError(err) {
function getConfigurationPageUrl (line 1256) | function getConfigurationPageUrl(page, tab = "") {
function onLinkRedirectClick (line 1271) | function onLinkRedirectClick(event) {
function overrideLink (line 1282) | function overrideLink(target) {
function renderCheckboxList (line 1298) | function renderCheckboxList(form, name, enabled) {
function retrieveCheckboxList (line 1311) | function retrieveCheckboxList(form, name) {
function onSortableContainerClick (line 1327) | function onSortableContainerClick(event) {
function overrideSortableCheckboxList (line 1363) | function overrideSortableCheckboxList(element) {
function adjustSortableListElement (line 1373) | function adjustSortableListElement(element, index) {
function getParentWithClass (line 1400) | function getParentWithClass(element, className) {
function renderSortableCheckboxList (line 1413) | function renderSortableCheckboxList(form, name, enabled, order) {
function retrieveSortableCheckboxList (line 1450) | function retrieveSortableCheckboxList(view, name) {
function escapeHtml (line 1491) | function escapeHtml(string) {
FILE: Shokofin/Pages/Scripts/Dummy.js
method onShow (line 28) | onShow(event) {
method onHide (line 42) | onHide() {
FILE: Shokofin/Pages/Scripts/Settings.js
method onInit (line 94) | onInit() {
method onShow (line 369) | async onShow() {
method onHide (line 385) | onHide() {
function updateView (line 400) | async function updateView(view, form, config) {
function updateSignalrStatus (line 553) | function updateSignalrStatus(form, status) {
function applyFormToConfig (line 581) | function applyFormToConfig(form, config) {
function applyTitleFormToConfig (line 774) | function applyTitleFormToConfig(form, config) {
function applyDescriptionFormToConfig (line 800) | function applyDescriptionFormToConfig(form, config) {
function applyImageFormToConfig (line 814) | function applyImageFormToConfig(form, config) {
function applyConfigToForm (line 837) | async function applyConfigToForm(form, config) {
function applyUserConfigToForm (line 1026) | async function applyUserConfigToForm(form, userId, config = null) {
function applySeriesConfigToForm (line 1088) | async function applySeriesConfigToForm(form, seriesId, config = null) {
function applyLibraryConfigToForm (line 1131) | async function applyLibraryConfigToForm(form, libraryId, config = null) {
function mediaFolderConfigToString (line 1175) | function mediaFolderConfigToString(c) {
function renderFolderList (line 1196) | function renderFolderList(form, disableButtons, name, entries) {
function applySignalrLibraryConfigToForm (line 1235) | async function applySignalrLibraryConfigToForm(form, libraryId, config =...
function addMediaFolder (line 1288) | function addMediaFolder(form, libraryId, config, path) {
function removeMediaFolder (line 1314) | function removeMediaFolder(form, libraryId, config, index) {
function toggleRefreshOfMediaFolder (line 1332) | function toggleRefreshOfMediaFolder(form, libraryId, config, index) {
function toggleIgnoredMediaFolder (line 1350) | function toggleIgnoredMediaFolder(form, libraryId, config, index) {
function defaultSubmit (line 1374) | async function defaultSubmit(form) {
function resetConnection (line 1445) | async function resetConnection(form) {
function syncSettings (line 1469) | async function syncSettings(form, config) {
function removeUserConfig (line 1533) | async function removeUserConfig(form) {
function removeAlternateTitle (line 1558) | async function removeAlternateTitle(form, index) {
function addAlternateTitle (line 1587) | async function addAlternateTitle(form) {
function renderAlternateTitles (line 1617) | function renderAlternateTitles(form, configAlternateTitles) {
function toggleExpertMode (line 1655) | async function toggleExpertMode(expertMode = false, debugMode = false) {
function filterIgnoredFolders (line 1685) | function filterIgnoredFolders(value) {
function filterReconnectIntervals (line 1705) | function filterReconnectIntervals(value) {
function sanitizeNumber (line 1717) | function sanitizeNumber(raw, min = 0, max = Number.MAX_SAFE_INTEGER) {
function filterTags (line 1731) | function filterTags(value) {
FILE: Shokofin/Plugin.cs
class Plugin (line 19) | public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
method Plugin (line 166) | public Plugin(UsageTracker usageTracker, IServerConfigurationManager c...
method UpdateConfiguration (line 227) | public void UpdateConfiguration() {
method OnConfigChanged (line 231) | private void OnConfigChanged(object? sender, BasePluginConfiguration e) {
method MigrateConfiguration (line 247) | private void MigrateConfiguration(PluginConfiguration config) {
method FixupConfiguration (line 364) | public void FixupConfiguration(PluginConfiguration config) {
method GetPages (line 394) | public IEnumerable<PluginPageInfo> GetPages() {
FILE: Shokofin/PluginServiceRegistrator.cs
class PluginServiceRegistrator (line 8) | public class PluginServiceRegistrator : IPluginServiceRegistrator {
method RegisterServices (line 10) | public void RegisterServices(IServiceCollection serviceCollection, ISe...
FILE: Shokofin/Providers/BoxSetProvider.cs
class BoxSetProvider (line 19) | public class BoxSetProvider(IHttpClientFactory _httpClientFactory, ILogg...
method GetMetadata (line 25) | public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info,...
method GetShokoSeriesMetadata (line 45) | private async Task<MetadataResult<BoxSet>> GetShokoSeriesMetadata(BoxS...
method GetShokoGroupMetadata (line 74) | private async Task<MetadataResult<BoxSet>> GetShokoGroupMetadata(BoxSe...
method GetSearchResults (line 99) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetIn...
method GetImageResponse (line 102) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/CustomBoxSetProvider.cs
class CustomBoxSetProvider (line 32) | public class CustomBoxSetProvider(ILogger<CustomBoxSetProvider> _logger,...
method HasChanged (line 36) | public bool HasChanged(BaseItem item, IDirectoryService directoryServi...
method FetchAsync (line 52) | public async Task<ItemUpdateType> FetchAsync(BoxSet collection, Metada...
method EnsureSeriesCollectionIsCorrect (line 73) | private async Task<bool> EnsureSeriesCollectionIsCorrect(BoxSet collec...
method EnsureGroupCollectionIsCorrect (line 98) | private async Task<bool> EnsureGroupCollectionIsCorrect(Folder collect...
method GetCollectionByCollectionId (line 127) | private async Task<BoxSet> GetCollectionByCollectionId(Folder collecti...
method GetCollectionByPath (line 155) | private BoxSet? GetCollectionByPath(Folder collectionRoot, CollectionI...
method EnsureNoTmdbIdIsSet (line 162) | private static bool EnsureNoTmdbIdIsSet(BoxSet collection) {
FILE: Shokofin/Providers/CustomEpisodeProvider.cs
class CustomEpisodeProvider (line 30) | public class CustomEpisodeProvider(
method HasChanged (line 42) | public bool HasChanged(BaseItem item, IDirectoryService directoryServi...
method FetchAsync (line 61) | public async Task<ItemUpdateType> FetchAsync(Episode episode, Metadata...
method RemoveVirtualEpisodes (line 103) | private bool RemoveVirtualEpisodes(string episodeId, Episode episode, ...
method EpisodeExists (line 132) | private static bool EpisodeExists(ILibraryManager libraryManager, ILog...
method AddVirtualEpisode (line 151) | public static bool AddVirtualEpisode(ILibraryManager libraryManager, I...
FILE: Shokofin/Providers/CustomMovieProvider.cs
class CustomMovieProvider (line 27) | public class CustomMovieProvider(
method HasChanged (line 39) | public bool HasChanged(BaseItem item, IDirectoryService directoryServi...
method FetchAsync (line 58) | public async Task<ItemUpdateType> FetchAsync(Movie movie, MetadataRefr...
FILE: Shokofin/Providers/CustomSeasonProvider.cs
class CustomSeasonProvider (line 31) | public class CustomSeasonProvider(ILogger<CustomSeasonProvider> _logger,...
method HasChanged (line 36) | public bool HasChanged(BaseItem item, IDirectoryService directoryServi...
method FetchAsync (line 60) | public async Task<ItemUpdateType> FetchAsync(Season season, MetadataRe...
method RemoveVirtualSeasons (line 198) | private static bool RemoveVirtualSeasons(ILibraryManager libraryManage...
method SeasonExists (line 225) | private static bool SeasonExists(ILibraryManager libraryManager, ILogg...
method AddVirtualSeasonZero (line 246) | public static Season? AddVirtualSeasonZero(ILibraryManager libraryMana...
method AddVirtualSeason (line 273) | public static Season? AddVirtualSeason(ILibraryManager libraryManager,...
FILE: Shokofin/Providers/CustomSeriesProvider.cs
class CustomSeriesProvider (line 32) | public class CustomSeriesProvider(ILogger<CustomSeriesProvider> _logger,...
method HasChanged (line 37) | public bool HasChanged(BaseItem item, IDirectoryService directoryServi...
method FetchAsync (line 55) | public async Task<ItemUpdateType> FetchAsync(Series series, MetadataRe...
method CreateMissingSeasons (line 246) | private IEnumerable<(int, Season)> CreateMissingSeasons(Info.ShowInfo ...
FILE: Shokofin/Providers/EpisodeProvider.cs
class EpisodeProvider (line 23) | public class EpisodeProvider(IHttpClientFactory _httpClientFactory, ILog...
method GetMetadata (line 28) | public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo inf...
method CreateMetadata (line 96) | public static Episode CreateMetadata(Info.ShowInfo showInfo, Info.Seas...
method CreateMetadata (line 99) | public static Episode CreateMetadata(Info.ShowInfo showInfo, Info.Seas...
method CreateMetadata (line 102) | private static Episode CreateMetadata(Info.ShowInfo showInfo, Info.Sea...
method GetSearchResults (line 251) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeI...
method GetImageResponse (line 254) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/ImageProvider.cs
class ImageProvider (line 21) | public class ImageProvider(IHttpClientFactory _httpClientFactory, ILogge...
method GetImages (line 26) | public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem ite...
method GetSupportedImages (line 119) | public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
method Supports (line 122) | public bool Supports(BaseItem item)
method GetImageResponse (line 125) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/MovieProvider.cs
class MovieProvider (line 17) | public class MovieProvider(IHttpClientFactory _httpClientFactory, ILogge...
method GetMetadata (line 22) | public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, C...
method GetSearchResults (line 88) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInf...
method GetImageResponse (line 91) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/SeasonProvider.cs
class SeasonProvider (line 21) | public class SeasonProvider(IHttpClientFactory _httpClientFactory, ILogg...
method GetMetadata (line 26) | public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info,...
method CreateMetadata (line 88) | public static Season CreateMetadata(Info.SeasonInfo seasonInfo, int se...
method CreateMetadata (line 91) | public static Season CreateMetadata(Info.SeasonInfo seasonInfo, int se...
method CreateMetadata (line 94) | private static Season CreateMetadata(Info.SeasonInfo seasonInfo, int s...
method GetSearchResults (line 157) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonIn...
method GetImageResponse (line 160) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/SeriesProvider.cs
class SeriesProvider (line 20) | public class SeriesProvider(IHttpClientFactory _httpClientFactory, ILogg...
method GetMetadata (line 25) | public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info,...
method GetSearchResults (line 104) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesIn...
method GetImageResponse (line 107) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/TrailerProvider.cs
class TrailerProvider (line 18) | public class TrailerProvider(IHttpClientFactory _httpClientFactory, ILog...
method GetMetadata (line 25) | public async Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo inf...
method GetSearchResults (line 74) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerI...
method GetImageResponse (line 77) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Providers/VideoProvider.cs
class VideoProvider (line 18) | public class VideoProvider(IHttpClientFactory _httpClientFactory, ILogge...
method GetMetadata (line 25) | public async Task<MetadataResult<Video>> GetMetadata(ItemLookupInfo in...
method GetSearchResults (line 78) | public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLook...
method GetImageResponse (line 81) | public async Task<HttpResponseMessage> GetImageResponse(string url, Ca...
FILE: Shokofin/Resolvers/Models/LinkGenerationResult.cs
class LinkGenerationResult (line 8) | public class LinkGenerationResult {
method Print (line 65) | public void Print(ILogger logger, string path) {
FILE: Shokofin/Resolvers/Models/ShokoWatcher.cs
class ShokoWatcher (line 8) | public class ShokoWatcher(MediaFolderConfiguration configuration, FileSy...
FILE: Shokofin/Resolvers/ShokoIgnoreRule.cs
class ShokoIgnoreRule (line 20) | public class ShokoIgnoreRule : IResolverIgnoreRule {
method ShokoIgnoreRule (line 35) | public ShokoIgnoreRule(
method ShouldFilterItem (line 53) | public async Task<bool> ShouldFilterItem(Folder? parent, FileSystemMet...
method ShouldFilterDirectory (line 125) | private async Task<bool> ShouldFilterDirectory(string partialPath, str...
method ShouldFilterFile (line 192) | private async Task<bool> ShouldFilterFile(string partialPath, string f...
method ShouldIgnore (line 224) | bool IResolverIgnoreRule.ShouldIgnore(FileSystemMetadata fileInfo, Bas...
FILE: Shokofin/Resolvers/ShokoLibraryMonitor.cs
class ShokoLibraryMonitor (line 25) | public class ShokoLibraryMonitor : IHostedService {
method ShokoLibraryMonitor (line 54) | public ShokoLibraryMonitor(
method StartAsync (line 90) | Task IHostedService.StartAsync(CancellationToken cancellationToken) {
method StopAsync (line 95) | Task IHostedService.StopAsync(CancellationToken cancellationToken) {
method StartWatching (line 100) | public void StartWatching() {
method StopWatching (line 116) | public void StopWatching() {
method OnLibraryScanRunningChanged (line 121) | private void OnLibraryScanRunningChanged(object? sender, bool isScanRu...
method OnLibraryConfigurationAddedOrChanged (line 128) | private void OnLibraryConfigurationAddedOrChanged(object? sender, Libr...
method OnLibraryConfigurationRemoved (line 148) | private void OnLibraryConfigurationRemoved(object? sender, LibraryConf...
method OnMediaFolderConfigurationAdded (line 157) | private void OnMediaFolderConfigurationAdded(object? sender, MediaConf...
method OnMediaFolderConfigurationRemoved (line 173) | private void OnMediaFolderConfigurationRemoved(object? sender, MediaCo...
method StartWatchingMediaFolder (line 181) | private void StartWatchingMediaFolder(MediaFolderConfiguration config) {
method StopWatchingPath (line 219) | private void StopWatchingPath(string path) {
method DisposeWatcher (line 225) | private void DisposeWatcher(FileSystemWatcher watcher, bool removeFrom...
method OnWatcherError (line 247) | private void OnWatcherError(object sender, ErrorEventArgs eventArgs) {
method OnWatcherChanged (line 257) | private void OnWatcherChanged(object? sender, FileSystemEventArgs e) {
method ReportFileSystemChanged (line 268) | public async Task ReportFileSystemChanged(MediaFolderConfiguration med...
method IsVideoFile (line 357) | private bool IsVideoFile(string path)
FILE: Shokofin/Resolvers/ShokoResolver.cs
class ShokoResolver (line 28) | public class ShokoResolver : IItemResolver, IMultiItemResolver {
method ShokoResolver (line 43) | public ShokoResolver(
method ResolveSingle (line 61) | public async Task<BaseItem?> ResolveSingle(Folder? parent, CollectionT...
method ResolveMultiple (line 98) | public async Task<MultiItemResolverResult> ResolveMultiple(Folder? par...
method ResolvePath (line 311) | public BaseItem? ResolvePath(ItemResolveArgs args)
method ResolvePath (line 317) | public BaseItem? ResolvePath(ItemResolveArgs args, CancellationToken c...
method ResolveMultiple (line 327) | public MultiItemResolverResult ResolveMultiple(Folder parent, List<Fil...
method ResolveMultiple (line 333) | public MultiItemResolverResult ResolveMultiple(Folder parent, List<Fil...
FILE: Shokofin/Resolvers/VirtualFileSystemService.cs
class VirtualFileSystemService (line 33) | public class VirtualFileSystemService {
method VirtualFileSystemService (line 81) | public VirtualFileSystemService(
method OnTrackerStalled (line 124) | private void OnTrackerStalled(object? sender, EventArgs eventArgs) {
method Clear (line 129) | public void Clear() {
method OnProviderManagerRefreshStarted (line 136) | private void OnProviderManagerRefreshStarted(object? sender, GenericEv...
method PreviewChangesForLibrary (line 159) | public async Task<(HashSet<string> filesBefore, HashSet<string> filesA...
method TryGetCurrentLibraryGenerationMode (line 210) | public bool TryGetCurrentLibraryGenerationMode(string? path, out bool ...
method GenerateStructureInVFS (line 240) | public async Task<(string? vfsPath, bool shouldContinue, bool skipVali...
method TryGetFileCheckerForMediaFolders (line 476) | private bool TryGetFileCheckerForMediaFolders(LibraryConfiguration lib...
method GetFloodSearchFileChecker (line 507) | private Func<string, bool> GetFloodSearchFileChecker(LibraryConfigurat...
method GetFilesForEpisode (line 534) | private IEnumerable<(string sourceLocation, string fileId, string seri...
method GetFilesForMovie (line 585) | private IEnumerable<(string sourceLocation, string fileId, string seri...
method GetFilesForShow (line 645) | private IEnumerable<(string sourceLocation, string fileId, string seri...
method GetFilesForManagedFolders (line 767) | private IEnumerable<(string sourceLocation, string fileId, string seri...
method GetManagedFolderFilesPage (line 909) | private async Task<ListResult<API.Models.File>> GetManagedFolderFilesP...
method GenerateStructure (line 914) | private async Task<LinkGenerationResult> GenerateStructure(CollectionT...
method GenerateLocationsForFile (line 1004) | public async Task<(string[] symbolicLinks, DateTime? importedAt)> Gene...
method GenerateSymbolicLinks (line 1135) | public LinkGenerationResult GenerateSymbolicLinks(string vfsPath, stri...
method FindExternalFilesForPath (line 1281) | private List<string> FindExternalFilesForPath(string sourcePath, Exter...
method EnsureCreationDateForDirectories (line 1307) | private void EnsureCreationDateForDirectories(string vfsPath, string p...
method LinkExternalFiles (line 1325) | private void LinkExternalFiles(string sourceLocation, string symbolicL...
method AddParentDirectories (line 1378) | private static HashSet<string> AddParentDirectories(string rootDirecto...
method CleanupStructure (line 1407) | private LinkGenerationResult CleanupStructure(string vfsPath, string d...
method TryMoveExternalFile (line 1573) | private bool TryMoveExternalFile(IReadOnlyList<string> allKnownPaths, ...
method TryMoveTrickplayDirectory (line 1657) | private bool TryMoveTrickplayDirectory(IReadOnlyList<string> allKnownP...
method CopyDirectory (line 1746) | private void CopyDirectory(string source, string destination) {
method ShouldIgnoreFile (line 1759) | private static bool ShouldIgnoreFile(string vfsPath, string path) {
method TryGetIdsForPath (line 1765) | public static bool TryGetIdsForPath(string path, [NotNullWhen(true)] o...
method ContainsFileSystemEntryPaths (line 1783) | private bool ContainsFileSystemEntryPaths(string directoryPath)
method GetFilePaths (line 1786) | public string[] GetFilePaths(string directoryPath, bool recursive = fa...
method GetFileSystemEntryPaths (line 1793) | public string[] GetFileSystemEntryPaths(string directoryPath, bool rec...
method GetFileSystemEntryPaths (line 1800) | private string[] GetFileSystemEntryPaths(string directoryPath, bool re...
method GetPathValidator (line 1834) | private static Func<string, bool, bool> GetPathValidator(IEnumerable<s...
method GetThreadCount (line 1847) | private int GetThreadCount()
method Parallelize (line 1854) | private Task Parallelize<T>(T initialValue, Func<T, IEnumerable<T>?> a...
method Parallelize (line 1883) | private Task Parallelize<T>(IEnumerable<T> items, Func<T, Task> action...
method Parallelize (line 1901) | private Task Parallelize<T>(IEnumerable<T> items, Action<T> action, Ca...
FILE: Shokofin/SignalR/Models/EpisodeInfoUpdatedEventArgs.cs
class EpisodeInfoUpdatedEventArgs (line 9) | public class EpisodeInfoUpdatedEventArgs : IMetadataUpdatedEventArgs {
FILE: Shokofin/SignalR/Models/FileEventArgs.cs
class FileEventArgs (line 7) | public class FileEventArgs : IFileEventArgs {
FILE: Shokofin/SignalR/Models/FileMovedEventArgs.cs
class FileMovedEventArgs (line 6) | public class FileMovedEventArgs: FileEventArgs, IFileRelocationEventArgs {
FILE: Shokofin/SignalR/Models/FileRenamedEventArgs.cs
class FileRenamedEventArgs (line 6) | public class FileRenamedEventArgs : FileEventArgs, IFileRelocationEventA...
FILE: Shokofin/SignalR/Models/MovieInfoUpdatedEventArgs.cs
class MovieInfoUpdatedEventArgs (line 9) | public class MovieInfoUpdatedEventArgs : IMetadataUpdatedEventArgs {
FILE: Shokofin/SignalR/Models/ReleaseSavedEventArgs.cs
class ReleaseSavedEventArgs (line 6) | public class ReleaseSavedEventArgs : IReleaseSavedEventArgs {
FILE: Shokofin/SignalR/Models/SeriesInfoUpdatedEventArgs.cs
class SeriesInfoUpdatedEventArgs (line 9) | public class SeriesInfoUpdatedEventArgs : IMetadataUpdatedEventArgs {
FILE: Shokofin/SignalR/SignalRConnectionManager.cs
class SignalRConnectionManager (line 21) | public class SignalRConnectionManager {
method SignalRConnectionManager (line 46) | public SignalRConnectionManager(
method ConnectAsync (line 60) | private async Task ConnectAsync(PluginConfiguration config) {
method OnReconnected (line 130) | private Task OnReconnected(string? connectionId) {
method OnReconnecting (line 135) | private Task OnReconnecting(Exception? exception) {
method OnDisconnected (line 140) | private Task OnDisconnected(Exception? exception) {
method DisconnectAsync (line 149) | public async Task DisconnectAsync() {
method ResetConnectionAsync (line 167) | public Task ResetConnectionAsync()
method ResetConnection (line 170) | private void ResetConnection(PluginConfiguration config, bool shouldCo...
method ResetConnectionAsync (line 173) | private async Task ResetConnectionAsync(PluginConfiguration config, bo...
method RunAsync (line 179) | public async Task RunAsync() {
method StopAsync (line 187) | public async Task StopAsync() {
method OnConfigurationChanged (line 192) | private void OnConfigurationChanged(object? sender, PluginConfiguratio...
method CanConnect (line 201) | private static bool CanConnect(PluginConfiguration config)
method ConstructKey (line 204) | private static string ConstructKey(PluginConfiguration config)
method OnFileMatched (line 213) | private void OnFileMatched(IFileEventArgs eventArgs) {
method OnReleaseSaved (line 235) | private async Task OnReleaseSaved(IReleaseSavedEventArgs eventArgs0) {
method OnFileRelocated (line 268) | private void OnFileRelocated(IFileRelocationEventArgs eventArgs) {
method OnFileDeleted (line 293) | private void OnFileDeleted(IFileEventArgs eventArgs) {
method OnInfoUpdated (line 319) | private void OnInfoUpdated(IMetadataUpdatedEventArgs eventArgs) {
FILE: Shokofin/SignalR/SignalREntryPoint.cs
class SignalREntryPoint (line 8) | public class SignalREntryPoint : IHostedService {
method SignalREntryPoint (line 11) | public SignalREntryPoint(SignalRConnectionManager connectionManager) =...
method StopAsync (line 13) | public Task StopAsync(CancellationToken cancellationToken)
method StartAsync (line 16) | public Task StartAsync(CancellationToken cancellationToken)
FILE: Shokofin/SignalR/SignalRRetryPolicy.cs
class SignalrRetryPolicy (line 6) | public class SignalrRetryPolicy(TimeSpan[] delays) : IRetryPolicy
method NextRetryDelay (line 8) | public TimeSpan? NextRetryDelay(RetryContext retryContext)
FILE: Shokofin/Sync/SyncDirection.cs
type SyncDirection (line 8) | [Flags]
FILE: Shokofin/Sync/UserDataSyncManager.cs
class UserDataSyncManager (line 28) | public class UserDataSyncManager {
method UserDataSyncManager (line 46) | public UserDataSyncManager(IUserDataManager userDataManager, IUserMana...
method Dispose (line 65) | public void Dispose() {
method TryGetUserConfiguration (line 75) | private static bool TryGetUserConfiguration(Guid userId, [NotNullWhen(...
class SessionMetadata (line 82) | internal class SessionMetadata {
method SessionMetadata (line 148) | public SessionMetadata(ILogger logger, SessionInfo sessionInfo, Guid...
method ShouldSendEvent (line 162) | public bool ShouldSendEvent(bool isPauseOrResumeEvent = false) {
method GetSessionsForSessionId (line 179) | private IEnumerable<SessionMetadata> GetSessionsForSessionId(string se...
method TryGetSessionByUserId (line 187) | private bool TryGetSessionByUserId(Guid userId, Guid itemId, [NotNullW...
method OnPlaybackStart (line 199) | public void OnPlaybackStart(object? sender, PlaybackProgressEventArgs ...
method OnPlaybackStopped (line 204) | public void OnPlaybackStopped(object? sender, PlaybackProgressEventArg...
method OnSessionStarted (line 209) | public void OnSessionStarted(object? sender, SessionEventArgs e) {
method OnSessionEnded (line 224) | public void OnSessionEnded(object? sender, SessionEventArgs e) {
method OnUserDataSaved (line 230) | public async void OnUserDataSaved(object? sender, UserDataSaveEventArg...
method OnUserRatingSaved (line 386) | private void OnUserRatingSaved(object? sender, UserDataSaveEventArgs e) {
method ScanAndSync (line 421) | public async Task ScanAndSync(SyncDirection direction, IProgress<doubl...
method OnItemAddedOrUpdated (line 462) | public void OnItemAddedOrUpdated(object? sender, ItemChangeEventArgs e) {
method SyncSeries (line 552) | private Task SyncSeries(Series series, UserConfiguration userConfig, U...
method SyncSeason (line 569) | private Task SyncSeason(Season season, UserConfiguration userConfig, U...
method SyncVideo (line 586) | private Task SyncVideo(Video video, UserConfiguration userConfig, User...
method SyncVideo (line 612) | private async Task SyncVideo(Video video, UserConfiguration userConfig...
method UserDataEqualsFileUserStats (line 711) | private static bool UserDataEqualsFileUserStats(UserItemData? localUse...
FILE: Shokofin/Tasks/AutoRefreshMetadataTask.cs
class AutoRefreshMetadataTask (line 14) | public class AutoRefreshMetadataTask(MetadataRefreshService _metadataRef...
method GetDefaultTriggers (line 37) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 41) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/CleanupVirtualRootTask.cs
class CleanupVirtualRootTask (line 20) | public class CleanupVirtualRootTask(
method GetDefaultTriggers (line 49) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 60) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/ClearPluginCacheTask.cs
class ClearPluginCacheTask (line 16) | public class ClearPluginCacheTask(
method GetDefaultTriggers (line 44) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 47) | public Task ExecuteAsync(IProgress<double> progress, CancellationToken...
FILE: Shokofin/Tasks/ExportUserDataTask.cs
class ExportUserDataTask (line 10) | public class ExportUserDataTask(UserDataSyncManager _userSyncManager) : ...
method GetDefaultTriggers (line 32) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 35) | public Task ExecuteAsync(IProgress<double> progress, CancellationToken...
FILE: Shokofin/Tasks/ImportUserDataTask.cs
class ImportUserDataTask (line 10) | public class ImportUserDataTask(UserDataSyncManager _userSyncManager) : ...
method GetDefaultTriggers (line 32) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 35) | public Task ExecuteAsync(IProgress<double> progress, CancellationToken...
FILE: Shokofin/Tasks/MergeEpisodesTask.cs
class MergeEpisodesTask (line 13) | public class MergeEpisodesTask(MergeVersionsManager _mergeVersionsManage...
method GetDefaultTriggers (line 36) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 40) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/MergeMoviesTask.cs
class MergeMoviesTask (line 13) | public class MergeMoviesTask(MergeVersionsManager _mergeVersionsManager)...
method GetDefaultTriggers (line 36) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 40) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/PostScanTask.cs
class PostScanTask (line 9) | public class PostScanTask(ITaskManager taskManager) : ILibraryPostScanTa...
method Run (line 11) | public Task Run(IProgress<double> progress, CancellationToken token) {
FILE: Shokofin/Tasks/ReconstructCollectionsTask.cs
class ReconstructCollectionsTask (line 13) | public class ReconstructCollectionsTask(CollectionManager _collectionMan...
method GetDefaultTriggers (line 36) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 40) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/SplitEpisodesTask.cs
class SplitEpisodesTask (line 14) | public class SplitEpisodesTask(MergeVersionsManager _mergeVersionsManage...
method GetDefaultTriggers (line 37) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 41) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/SplitMoviesTask.cs
class SplitMoviesTask (line 14) | public class SplitMoviesTask(MergeVersionsManager _mergeVersionsManager,...
method GetDefaultTriggers (line 37) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 41) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/SyncUserDataTask.cs
class SyncUserDataTask (line 10) | public class SyncUserDataTask(UserDataSyncManager _userSyncManager) : IS...
method GetDefaultTriggers (line 33) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 37) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Tasks/VersionCheckTask.cs
class VersionCheckTask (line 18) | public class VersionCheckTask(ILogger<VersionCheckTask> _logger, ILibrar...
method GetDefaultTriggers (line 40) | public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
method ExecuteAsync (line 52) | public async Task ExecuteAsync(IProgress<double> progress, Cancellatio...
FILE: Shokofin/Utils/ContentRating.cs
class ContentRating (line 15) | public static class ContentRating {
class TvContentIndicatorsAttribute (line 16) | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | A...
type TvRating (line 27) | public enum TvRating {
type TvContentIndicator (line 126) | public enum TvContentIndicator {
method GetOrderedProviders (line 149) | private static ProviderName[] GetOrderedProviders()
method GetContentRating (line 152) | public static string? GetContentRating(IExtendedItemInfo seasonInfo, s...
method GetCombinedAnidbContentRating (line 166) | public static string? GetCombinedAnidbContentRating(IEnumerable<Season...
method GetTagBasedContentRating (line 180) | public static string? GetTagBasedContentRating(IReadOnlyDictionary<str...
method TryConvertRatingFromText (line 270) | private static bool TryConvertRatingFromText(string? value, out TvRati...
method GetCustomAttributes (line 334) | internal static T[] GetCustomAttributes<T>(this System.Reflection.Fiel...
method ConvertRatingToText (line 337) | private static string? ConvertRatingToText(TvRating value, IEnumerable...
FILE: Shokofin/Utils/DisposableAction.cs
class DisposableAction (line 6) | public class DisposableAction : IDisposable {
method DisposableAction (line 9) | public DisposableAction(Action disposeAction) {
method Dispose (line 13) | public void Dispose()
FILE: Shokofin/Utils/GuardedMemoryCache.cs
class GuardedMemoryCache (line 11) | internal class GuardedMemoryCache : IDisposable, IMemoryCache {
method GuardedMemoryCache (line 24) | public GuardedMemoryCache(ILogger logger, MemoryCacheOptions options, ...
method Clear (line 31) | public void Clear() {
method GetOrCreate (line 44) | public TItem GetOrCreate<TItem>(object key, Action<TItem> foundAction,...
method GetOrCreate (line 91) | public TItem GetOrCreate<TItem>(object key, Action<TItem> foundAction,...
method GetOrCreateAsync (line 138) | public async Task<TItem> GetOrCreateAsync<TItem>(object key, Action<TI...
method GetOrCreateAsync (line 185) | public async Task<TItem> GetOrCreateAsync<TItem>(object key, Action<TI...
method GetOrCreate (line 232) | public TItem GetOrCreate<TItem>(object key, Func<TItem> createFactory,...
method GetOrCreate (line 274) | public TItem GetOrCreate<TItem>(object key, Func<GuardedMemoryCacheEnt...
method GetOrCreateAsync (line 316) | public async Task<TItem> GetOrCreateAsync<TItem>(object key, Func<Task...
method GetOrCreateAsync (line 358) | public async Task<TItem> GetOrCreateAsync<TItem>(object key, Func<Guar...
method CreateNewOptions (line 400) | private GuardedMemoryCacheEntryOptions CreateNewOptions()
method Dispose (line 409) | public void Dispose() {
method CreateEntry (line 414) | public ICacheEntry CreateEntry(object key)
method Remove (line 417) | public void Remove(object key)
method TryGetValue (line 420) | public bool TryGetValue(object key, [NotNullWhen(true)] out object? va...
method TryGetValue (line 423) | public bool TryGetValue<TItem>(object key, [NotNullWhen(true)] out TIt...
method Set (line 426) | public TItem? Set<TItem>(object key, [NotNullIfNotNull(nameof(value))]...
class GuardedMemoryCacheEntryOptions (line 429) | internal class GuardedMemoryCacheEntryOptions : MemoryCacheEntryOptions {
FILE: Shokofin/Utils/IgnorePatterns.cs
class IgnorePatterns (line 14) | public static class IgnorePatterns {
method ShouldIgnore (line 117) | public static bool ShouldIgnore(ReadOnlySpan<char> path) {
FILE: Shokofin/Utils/ImageUtility.cs
class ImageUtility (line 15) | public static class ImageUtility {
method GetEpisodeImages (line 18) | public static async Task<IReadOnlyCollection<RemoteImageInfo>> GetEpis...
method GetSeasonImages (line 46) | public static async Task<IReadOnlyCollection<RemoteImageInfo>> GetSeas...
method GetShowImages (line 74) | public static async Task<IReadOnlyCollection<RemoteImageInfo>> GetShow...
method CombineImages (line 107) | private static API.Models.Images CombineImages(IEnumerable<API.Models....
method GetMovieImages (line 126) | public static async Task<IReadOnlyCollection<RemoteImageInfo>> GetMovi...
method GetCollectionImages (line 154) | public static async Task<IReadOnlyCollection<RemoteImageInfo>> GetColl...
method GetCollectionImages (line 173) | public static async Task<IReadOnlyCollection<RemoteImageInfo>> GetColl...
method ProcessEpisodeImages (line 191) | private static IEnumerable<RemoteImageInfo> ProcessEpisodeImages(API.M...
method ProcessSeriesImages (line 201) | private static IEnumerable<RemoteImageInfo> ProcessSeriesImages(API.Mo...
method ProcessImages (line 216) | private static IEnumerable<RemoteImageInfo> ProcessImages(IReadOnlyLis...
method GetTypeForImage (line 252) | private static ImageLanguageType GetTypeForImage(API.Models.Image imag...
method SelectImage (line 268) | private static RemoteImageInfo? SelectImage(API.Models.Image? image, I...
FILE: Shokofin/Utils/LibraryScanWatcher.cs
class LibraryScanWatcher (line 6) | public class LibraryScanWatcher {
method LibraryScanWatcher (line 17) | public LibraryScanWatcher(ILibraryManager libraryManager) {
method OnLibraryScanRunningChanged (line 29) | private void OnLibraryScanRunningChanged(object? sender, bool isScanRu...
FILE: Shokofin/Utils/Ordering.cs
class Ordering (line 12) | public class Ordering {
type LibraryOperationMode (line 16) | public enum LibraryOperationMode {
type CollectionCreationType (line 47) | public enum CollectionCreationType {
type OrderType (line 68) | public enum OrderType {
type SpecialOrderType (line 95) | public enum SpecialOrderType {
method GetEpisodeNumber (line 137) | public static int GetEpisodeNumber(ShowInfo showInfo, SeasonInfo seaso...
method GetSpecialPlacement (line 174) | public static (int?, int?, int?, bool) GetSpecialPlacement(ShowInfo sh...
method GetSeasonNumber (line 227) | public static int GetSeasonNumber(ShowInfo showInfo, SeasonInfo season...
method GetExtraType (line 242) | public static ExtraType? GetExtraType(AnidbEpisode episode) {
FILE: Shokofin/Utils/PropertyWatcher.cs
class PropertyWatcher (line 6) | public class PropertyWatcher<T> {
method PropertyWatcher (line 15) | public PropertyWatcher(Func<T> valueGetter) {
method StartMonitoring (line 20) | public void StartMonitoring(int delayInSeconds) {
method StopMonitoring (line 32) | public void StopMonitoring() {
method CheckForChange (line 36) | private void CheckForChange() {
FILE: Shokofin/Utils/SeriesInfoRelationComparer.cs
class SeriesInfoRelationComparer (line 9) | public class SeriesInfoRelationComparer(bool useIndirect) : IComparer<Se...
method Compare (line 26) | public int Compare(SeasonInfo? a, SeasonInfo? b) {
method CompareDirectRelations (line 52) | private static int CompareDirectRelations(SeasonInfo a, SeasonInfo b) {
method CompareIndirectRelations (line 71) | private static int CompareIndirectRelations(SeasonInfo a, SeasonInfo b) {
method CompareAirDates (line 105) | private static int CompareAirDates(DateTime? a, DateTime? b)
FILE: Shokofin/Utils/TagFilter.cs
class TagFilter (line 12) | public static class TagFilter {
class TagSourceIncludeAttribute (line 16) | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | A...
method TagSourceIncludeAttribute (line 20) | public TagSourceIncludeAttribute(params string[] values) {
class TagSourceIncludeOnlyAttribute (line 28) | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | A...
method TagSourceIncludeOnlyAttribute (line 32) | public TagSourceIncludeOnlyAttribute(params string[] values) {
class TagSourceExcludeOnlyAttribute (line 40) | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | A...
method TagSourceExcludeOnlyAttribute (line 44) | public TagSourceExcludeOnlyAttribute(params string[] values) {
class TagSourceExcludeAttribute (line 52) | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | A...
method TagSourceExcludeAttribute (line 56) | public TagSourceExcludeAttribute(params string[] values) {
type TagSource (line 64) | [Flags]
type TagIncludeFilter (line 311) | [Flags]
type TagWeight (line 323) | [JsonConverter(typeof(JsonStringEnumConverter))]
method GetOrderedProductionLocationProviders (line 334) | private static ProviderName[] GetOrderedProductionLocationProviders()
method GetProductionLocations (line 337) | public static string[] GetProductionLocations(IExtendedItemInfo season...
method FilterTags (line 345) | public static string[] FilterTags(IReadOnlyDictionary<string, Resolved...
method FilterGenres (line 350) | public static string[] FilterGenres(IReadOnlyDictionary<string, Resolv...
method FilterInternal (line 359) | private static string[] FilterInternal(IReadOnlyDictionary<string, Res...
method GetTagsFromSource (line 381) | private static HashSet<string> GetTagsFromSource(IReadOnlyDictionary<s...
method GetSourceMaterial (line 442) | private static string GetSourceMaterial(IReadOnlyDictionary<string, Re...
method GetProductionCountriesFromTags (line 470) | public static string[] GetProductionCountriesFromTags(IReadOnlyDiction...
method SelectTagName (line 502) | private static string SelectTagName(ResolvedTag tag)
method HasAnyFlags (line 505) | private static bool HasAnyFlags(this Enum value, params Enum[] candida...
FILE: Shokofin/Utils/TextUtility.cs
class TextUtility (line 15) | public static partial class TextUtility {
method InvalidEpisodeTitleRegex (line 88) | [GeneratedRegex(@"^(?:Special|Episode|Volume|OVA|OAD|Web) \d+$|^Part \...
method InvalidSeriesOrSeasonTitleRegex (line 94) | [GeneratedRegex(@"^\s*((?:(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s...
type DescriptionProvider (line 100) | public enum DescriptionProvider {
type DescriptionConversionMode (line 127) | public enum DescriptionConversionMode {
type TitleProvider (line 147) | public enum TitleProvider {
method JoinText (line 186) | public static string? JoinText(IEnumerable<string?> textList) {
method GetEpisodeDescription (line 214) | public static string GetEpisodeDescription(EpisodeInfo episodeInfo, Se...
method GetEpisodeDescription (line 233) | public static string GetEpisodeDescription(IEnumerable<EpisodeInfo> ep...
method GetSeasonDescription (line 240) | public static string GetSeasonDescription(SeasonInfo seasonInfo, strin...
method GetShowDescription (line 263) | public static string GetShowDescription(ShowInfo showInfo, string? met...
method GetMovieDescription (line 286) | public static string GetMovieDescription(EpisodeInfo episodeInfo, Seas...
method GetCollectionDescription (line 298) | public static string GetCollectionDescription(SeasonInfo seasonInfo, s...
method GetCollectionDescription (line 312) | public static string GetCollectionDescription(CollectionInfo collectio...
method GetDescription (line 319) | private static string GetDescription(IBaseItemInfo baseInfo, Descripti...
method AppendNotes (line 336) | private static string AppendNotes(IBaseItemInfo baseInfo, DescriptionC...
method SanitizeAnidbDescription (line 370) | [MethodImpl(MethodImplOptions.AggressiveInlining)]
method SanitizeAnidbDescription (line 383) | public static string SanitizeAnidbDescription(string summary, out IRea...
method GetEpisodeTitles (line 433) | public static (string? displayTitle, string? alternateTitle) GetEpisod...
method GetEpisodeTitleByType (line 451) | private static string? GetEpisodeTitleByType(EpisodeInfo episodeInfo, ...
method GetSeasonTitles (line 483) | public static (string? displayTitle, string? alternateTitle) GetSeason...
method GetShowTitles (line 521) | public static (string? displayTitle, string? alternateTitle) GetShowTi...
method GetSeriesTitleByType (line 539) | private static string? GetSeriesTitleByType(IBaseItemInfo baseInfo, Ti...
method GetMovieTitles (line 570) | public static (string? displayTitle, string? alternateTitle) GetMovieT...
method GetMovieTitleByType (line 588) | private static string? GetMovieTitleByType(EpisodeInfo episodeInfo, Se...
method GetCollectionTitles (line 606) | public static (string? displayTitle, string? alternateTitle) GetCollec...
method GetCollectionTitles (line 621) | public static (string? displayTitle, string? alternateTitle) GetCollec...
method GetTitleForLanguage (line 644) | public static string? GetTitleForLanguage(IReadOnlyList<Title> titles,...
method GetMainLanguage (line 673) | private static string GetMainLanguage(IEnumerable<Title> titles)
method GuessOriginLanguage (line 681) | internal static string[] GuessOriginLanguage(IBaseItemInfo baseItemInf...
method NumericToRoman (line 691) | private static string NumericToRoman(int number) =>
method JoinTitles (line 720) | private static string? JoinTitles(IEnumerable<string?> titleList, stri...
FILE: Shokofin/Utils/UsageTracker.cs
class UsageTracker (line 8) | public class UsageTracker {
method UsageTracker (line 23) | public UsageTracker(ILogger<UsageTracker> logger) {
method UpdateTimeout (line 38) | public void UpdateTimeout(TimeSpan timeout) {
method OnTimerElapsed (line 59) | private void OnTimerElapsed(object? sender, ElapsedEventArgs eventArgs) {
method Enter (line 64) | public IDisposable Enter(string name) {
method Add (line 69) | public Guid Add(string name) {
method Remove (line 85) | public void Remove(Guid trackerId) {
FILE: Shokofin/Web/ImageHostUrl.cs
class ImageHostUrl (line 12) | public class ImageHostUrl : IAsyncActionFilter {
method OnActionExecutionAsync (line 49) | public async Task OnActionExecutionAsync(ActionExecutingContext contex...
FILE: Shokofin/Web/Models/SimpleSeries.cs
class SimpleSeries (line 7) | public class SimpleSeries {
FILE: Shokofin/Web/Models/VfsLibraryPreview.cs
class VfsLibraryPreview (line 10) | public class VfsLibraryPreview(HashSet<string> filesBefore, HashSet<stri...
class VfsLibraryPreviewStats (line 33) | public class VfsLibraryPreviewStats(LinkGenerationResult? result) {
FILE: Shokofin/Web/ShokofinHostController.cs
class ShokofinHostController (line 23) | [Authorize]
method GetVersionAsync (line 36) | [HttpGet("Version")]
method GetApiKeyAsync (line 55) | [HttpPost("GetApiKey")]
method GetImageAsync (line 77) | [AllowAnonymous]
class ApiLoginRequest (line 96) | public class ApiLoginRequest {
FILE: Shokofin/Web/ShokofinSignalRController.cs
class ShokofinSignalRController (line 20) | [Authorize]
method GetStatus (line 32) | [HttpGet("Status")]
method ConnectAsync (line 44) | [HttpPost("Connect")]
method DisconnectAsync (line 59) | [HttpPost("Disconnect")]
class ShokoSignalRStatus (line 72) | public class ShokoSignalRStatus {
FILE: Shokofin/Web/ShokofinUtilityController.cs
class ShokofinUtilityController (line 27) | [Authorize]
method PreviewVFS (line 47) | [HttpPost("VFS/Library/{libraryId}/Preview")]
method GetSeriesList (line 67) | [HttpGet("Series")]
method GetSeriesListWithQueryInternal (line 95) | private async Task<IReadOnlyList<SimpleSeries>> GetSeriesListWithQuery...
method GetSeriesListInternal (line 116) | private Task<IReadOnlyList<SimpleSeries>> GetSeriesListInternal()
method GetSeriesByShokoSeriesId (line 138) | private async Task<SimpleSeries?> GetSeriesByShokoSeriesId(int seriesI...
method GetSeriesByAnidbId (line 151) | private async Task<SimpleSeries?> GetSeriesByAnidbId(int anidbId) {
method IdRegex (line 164) | [GeneratedRegex(@"^\s*(?<type>[as])(?<id>\d+)\s*$")]
method GetSeriesConfigurationForId (line 172) | [HttpGet("Series/{seriesId}/Configuration")]
method UpdateSeriesConfigurationForId (line 195) | [HttpPost("Series/{seriesId}/Configuration")]
method UpdateSeriesConfigurationForId (line 215) | [HttpPut("Series/{seriesId}/Configuration")]
method GetShowInfoForSeriesId (line 229) | [HttpGet("Series/{seriesId}/ShowInfo")]
FILE: Shokofin/Web/VfsActionFilter.cs
class VfsActionFilter (line 9) | public class VfsActionFilter : IAsyncActionFilter {
method OnActionExecutionAsync (line 10) | public async Task OnActionExecutionAsync(ActionExecutingContext contex...
FILE: build_plugin.py
function extract_target_framework (line 8) | def extract_target_framework(csproj_path):
function extract_packages_to_output (line 20) | def extract_packages_to_output(csproj_path, framework):
function extract_target_abi (line 31) | def extract_target_abi(csproj_path, framework):
Condensed preview — 201 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,620K chars).
[
{
"path": ".config/dotnet-tools.json",
"chars": 154,
"preview": "{\n \"version\": 1,\n \"isRoot\": true,\n \"tools\": {\n \"dotnet-ef\": {\n \"version\": \"9.0.9\",\n \"commands\": [\n "
},
{
"path": ".github/ISSUE_TEMPLATE/bug.yml",
"chars": 2911,
"preview": "name: Shokofin Bug Report 101\ndescription: Report any bugs here!\nlabels: []\nprojects: []\nassignees: []\nbody:\n - type: m"
},
{
"path": ".github/ISSUE_TEMPLATE/features.yml",
"chars": 994,
"preview": "name: Shokofin Feature Request 101\r\ndescription: Request your features here!\r\nlabels: []\r\nprojects: []\r\nassignees: []\r\nb"
},
{
"path": ".github/workflows/changelog.jq",
"chars": 384,
"preview": "\nreduce .[] as $commit (\n \"\";\n . +\n \"### `\\($commit.type)`: \\($commit.subject). (\\($commit.commit)) @\\($commit.author"
},
{
"path": ".github/workflows/git-log-json.mjs",
"chars": 6804,
"preview": "#! /bin/env node\nimport { dirname, join } from \"node:path\";\nimport { execSync } from \"node:child_process\";\nimport { exis"
},
{
"path": ".github/workflows/release-daily.yml",
"chars": 6461,
"preview": "name: Build & Publish Dev Release\n\non:\n push:\n branches: \n - dev\n\njobs:\n current_info:\n runs-on: ubuntu-lates"
},
{
"path": ".github/workflows/release.yml",
"chars": 2779,
"preview": "name: Build Stable Release\n\non:\n release:\n types:\n - released\n\njobs:\n current_info:\n runs-on: ubuntu-latest"
},
{
"path": ".github/workflows/release_draft.jq",
"chars": 547,
"preview": "group_by(.simple_type) |\n\nsort_by(.[0].simple_type | ({ \"feat\": 0, \"change\": 1, \"fix\": 2, \"repo\": 3}[.] // 99)) |\n\nreduc"
},
{
"path": ".gitignore",
"chars": 670,
"preview": " # Common IntelliJ Platform excludes\n\n# User specific\n**/.idea/**/workspace.xml\n**/.idea/**/tasks.xml\n**/.idea/shelf/*\n*"
},
{
"path": ".vscode/extensions.json",
"chars": 303,
"preview": "{\n \"recommendations\": [\n \"ms-dotnettools.csharp\",\n \"editorconfig.editorconfig\",\n \"github.vscode-github-actions"
},
{
"path": ".vscode/settings.json",
"chars": 892,
"preview": "{\n \"editor.tabSize\": 4,\n \"files.trimTrailingWhitespace\": false,\n \"files.trimFinalNewlines\": false,\n \"files.insertFin"
},
{
"path": "LICENSE",
"chars": 1089,
"preview": "MIT License\n\nCopyright (c) 2020 Shoko - Anime Cataloging Program\n\nPermission is hereby granted, free of charge, to any p"
},
{
"path": "README.md",
"chars": 9775,
"preview": "# Shokofin\n\nA Jellyfin plugin to integrate [Jellyfin](https://jellyfin.org/) with\n[Shoko Server](https://shokoanime.com/"
},
{
"path": "Shokofin/API/Converters/JsonAutoStringConverter.cs",
"chars": 1193,
"preview": "\nusing System;\nusing System.Globalization;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Shok"
},
{
"path": "Shokofin/API/IdPrefix.cs",
"chars": 189,
"preview": "\nnamespace Shokofin.API;\n\ninternal struct IdPrefix {\n internal const char TmdbShow = 't';\n\n internal const char Tm"
},
{
"path": "Shokofin/API/Info/AniDB/AnidbAnimeInfo.cs",
"chars": 124,
"preview": "\nnamespace Shokofin.API.Info.AniDB;\n\npublic class AnidbAnimeInfo {\n public required string AnidbAnimeId { get; init; "
},
{
"path": "Shokofin/API/Info/AniDB/AnidbEpisodeInfo.cs",
"chars": 427,
"preview": "using Shokofin.API.Models;\nusing Shokofin.Extensions;\n\nnamespace Shokofin.API.Info.AniDB;\n\npublic class AnidbEpisodeInfo"
},
{
"path": "Shokofin/API/Info/CollectionInfo.cs",
"chars": 3441,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Shokofin.API.Models;\nusing Shokofin.API.Models."
},
{
"path": "Shokofin/API/Info/EpisodeInfo.cs",
"chars": 19980,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Threading;\nu"
},
{
"path": "Shokofin/API/Info/FileInfo.cs",
"chars": 1121,
"preview": "using System.Collections.Generic;\nusing System.Linq;\nusing Shokofin.API.Models;\nusing Shokofin.ExternalIds;\n\nnamespace S"
},
{
"path": "Shokofin/API/Info/IBaseItemInfo.cs",
"chars": 1373,
"preview": "using System;\nusing System.Collections.Generic;\nusing Shokofin.API.Models;\n\nnamespace Shokofin.API.Info;\n\n/// <summary>\n"
},
{
"path": "Shokofin/API/Info/IExtendedItemInfo.cs",
"chars": 574,
"preview": "using System.Collections.Generic;\nusing MediaBrowser.Controller.Entities;\nusing Shokofin.API.Models;\nusing Shokofin.Even"
},
{
"path": "Shokofin/API/Info/SeasonInfo.cs",
"chars": 32644,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/API/Info/Shoko/ShokoEpisodeInfo.cs",
"chars": 189,
"preview": "\nnamespace Shokofin.API.Info.Shoko;\n\npublic class ShokoEpisodeInfo {\n public required string ShokoEpisodeId { get; in"
},
{
"path": "Shokofin/API/Info/Shoko/ShokoSeriesInfo.cs",
"chars": 246,
"preview": "\nnamespace Shokofin.API.Info.Shoko;\n\npublic class ShokoSeriesInfo {\n public required string ShokoSeriesId { get; init"
},
{
"path": "Shokofin/API/Info/ShowInfo.cs",
"chars": 28385,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Threading;\nu"
},
{
"path": "Shokofin/API/Info/TMDB/TmdbEpisodeInfo.cs",
"chars": 838,
"preview": "\nusing System.Diagnostics.CodeAnalysis;\n\nnamespace Shokofin.API.Info.TMDB;\n\npublic class TmdbEpisodeInfo {\n public re"
},
{
"path": "Shokofin/API/Info/TMDB/TmdbMovieInfo.cs",
"chars": 694,
"preview": "\nusing System;\n\nnamespace Shokofin.API.Info.TMDB;\n\npublic class TmdbMovieInfo : IComparable<TmdbMovieInfo>, IEquatable<T"
},
{
"path": "Shokofin/API/Info/TMDB/TmdbSeasonInfo.cs",
"chars": 526,
"preview": "\nnamespace Shokofin.API.Info.TMDB;\n\npublic class TmdbSeasonInfo {\n public required string TmdbShowId { get; init; }\n\n"
},
{
"path": "Shokofin/API/Info/TMDB/TmdbShowInfo.cs",
"chars": 810,
"preview": "\nusing System;\n\nnamespace Shokofin.API.Info.TMDB;\n\npublic class TmdbShowInfo : IComparable<TmdbShowInfo>, IEquatable<Tmd"
},
{
"path": "Shokofin/API/Models/AniDB/AnidbAnime.cs",
"chars": 3661,
"preview": "\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models.An"
},
{
"path": "Shokofin/API/Models/AniDB/AnidbEpisode.cs",
"chars": 990,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Shokofin.API.Info.AniDB;\n\nna"
},
{
"path": "Shokofin/API/Models/ApiException.cs",
"chars": 3113,
"preview": "\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing Syste"
},
{
"path": "Shokofin/API/Models/ApiKey.cs",
"chars": 247,
"preview": "\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class ApiKey {\n /// <summary>\n /// "
},
{
"path": "Shokofin/API/Models/ComponentVersion.cs",
"chars": 1397,
"preview": "using System;\nusing System.ComponentModel;\nusing System.Linq;\nusing System.Text.Json.Serialization;\nusing Shokofin.Exten"
},
{
"path": "Shokofin/API/Models/ContentRating.cs",
"chars": 1204,
"preview": "using System;\n\nnamespace Shokofin.API.Models;\n\npublic class ContentRating : IEquatable<ContentRating> {\n /// <summary"
},
{
"path": "Shokofin/API/Models/CrossReference.cs",
"chars": 2942,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Shokofin.API.Models.Shoko;\n\nnamespace Shok"
},
{
"path": "Shokofin/API/Models/EpisodeType.cs",
"chars": 1522,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\n[JsonConverter(typeof(JsonStringEnumConverter))]\n"
},
{
"path": "Shokofin/API/Models/File.cs",
"chars": 5570,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\np"
},
{
"path": "Shokofin/API/Models/IDs.cs",
"chars": 156,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class IDs {\n [JsonPropertyName(\"ID\")]\n "
},
{
"path": "Shokofin/API/Models/Image.cs",
"chars": 4252,
"preview": "using System;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class Image {\n /// <summa"
},
{
"path": "Shokofin/API/Models/Images.cs",
"chars": 392,
"preview": "using System.Collections.Generic;\n\nnamespace Shokofin.API.Models;\n\npublic class Images {\n public List<Image> Posters "
},
{
"path": "Shokofin/API/Models/ListResult.cs",
"chars": 636,
"preview": "\nusing System.Collections.Generic;\n\nnamespace Shokofin.API.Models;\n\n/// <summary>\n/// A list with the total count of <ty"
},
{
"path": "Shokofin/API/Models/ManagedFolder.cs",
"chars": 370,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class ManagedFolder {\n /// <summary>\n "
},
{
"path": "Shokofin/API/Models/Rating.cs",
"chars": 1178,
"preview": "namespace Shokofin.API.Models;\n\npublic class Rating {\n /// <summary>\n /// The rating value relative to the <see cr"
},
{
"path": "Shokofin/API/Models/Relation.cs",
"chars": 2882,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\n/// <summary>\n/// Describes relations between two"
},
{
"path": "Shokofin/API/Models/ReleaseGroup.cs",
"chars": 731,
"preview": "using System.Text.Json.Serialization;\nusing Shokofin.API.Converters;\n\nnamespace Shokofin.API.Models;\n\npublic class Relea"
},
{
"path": "Shokofin/API/Models/ReleaseInfo.cs",
"chars": 757,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class ReleaseInfo {\n /// <summary>\n "
},
{
"path": "Shokofin/API/Models/ReleaseSource.cs",
"chars": 296,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\n[JsonConverter(typeof(JsonStringEnumConverter))]\n"
},
{
"path": "Shokofin/API/Models/Role.cs",
"chars": 4531,
"preview": "using System;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class Role : IEquatable<Role"
},
{
"path": "Shokofin/API/Models/SeriesType.cs",
"chars": 1208,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\n[JsonConverter(typeof(JsonStringEnumConverter))]\n"
},
{
"path": "Shokofin/API/Models/Shoko/ShokoEpisode.cs",
"chars": 2627,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Shokofin.API.Info.Shoko;\nusi"
},
{
"path": "Shokofin/API/Models/Shoko/ShokoGroup.cs",
"chars": 1958,
"preview": "\nusing System;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models.Shoko;\n\npublic class ShokoGroup {\n "
},
{
"path": "Shokofin/API/Models/Shoko/ShokoSeries.cs",
"chars": 5017,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Shokofin.API.Models.AniDB;\n\n"
},
{
"path": "Shokofin/API/Models/Studio.cs",
"chars": 1090,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\n/// <summary>\n/"
},
{
"path": "Shokofin/API/Models/TMDB/AlternateOrderingType.cs",
"chars": 216,
"preview": "\nnamespace Shokofin.API.Models.TMDB;\n\npublic enum AlternateOrderingType {\n Unknown = 0,\n OriginalAirDate = 1,\n "
},
{
"path": "Shokofin/API/Models/TMDB/ITmdbEntity.cs",
"chars": 1050,
"preview": "using System;\nusing System.Collections.Generic;\nusing Jellyfin.Data.Enums;\n\nnamespace Shokofin.API.Models.TMDB;\n\npublic "
},
{
"path": "Shokofin/API/Models/TMDB/ITmdbParentEntity.cs",
"chars": 1205,
"preview": "using System.Collections.Generic;\n\nnamespace Shokofin.API.Models.TMDB;\n\npublic interface ITmdbParentEntity : ITmdbEntity"
},
{
"path": "Shokofin/API/Models/TMDB/TmdbEpisode.cs",
"chars": 6108,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json.Serialization;\nusing Jellyfin."
},
{
"path": "Shokofin/API/Models/TMDB/TmdbEpisodeCrossReference.cs",
"chars": 1228,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models.TMDB;\n\n/// <summary>\n/// APIv3 The Movie DataBase ("
},
{
"path": "Shokofin/API/Models/TMDB/TmdbMovie.cs",
"chars": 4534,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\nusing S"
},
{
"path": "Shokofin/API/Models/TMDB/TmdbMovieCollection.cs",
"chars": 1472,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\n\nnamesp"
},
{
"path": "Shokofin/API/Models/TMDB/TmdbMovieCrossReference.cs",
"chars": 785,
"preview": "\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models.TMDB;\n\n/// <summary>\n/// APIv3 The Movie DataBase "
},
{
"path": "Shokofin/API/Models/TMDB/TmdbSeason.cs",
"chars": 2691,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\nusing S"
},
{
"path": "Shokofin/API/Models/TMDB/TmdbShow.cs",
"chars": 3923,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\nusing S"
},
{
"path": "Shokofin/API/Models/Tag.cs",
"chars": 8483,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json.Serialization;\n\nusing TagWeigh"
},
{
"path": "Shokofin/API/Models/Text.cs",
"chars": 1154,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class Text {\n /// <summary>\n /// The"
},
{
"path": "Shokofin/API/Models/Title.cs",
"chars": 303,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class Title : Text {\n /// <summary>\n "
},
{
"path": "Shokofin/API/Models/TitleType.cs",
"chars": 265,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\n[JsonConverter(typeof(JsonStringEnumConverter))]\n"
},
{
"path": "Shokofin/API/Models/YearlySeason.cs",
"chars": 1041,
"preview": "using System;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.API.Models;\n\npublic class YearlySeason : ICompar"
},
{
"path": "Shokofin/API/Models/YearlySeasonName.cs",
"chars": 522,
"preview": "\nnamespace Shokofin.API.Models;\n\n/// <summary>\n/// The name of a yearly season.\n/// </summary>\npublic enum YearlySeasonN"
},
{
"path": "Shokofin/API/ShokoApiClient.cs",
"chars": 45333,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System"
},
{
"path": "Shokofin/API/ShokoApiManager.cs",
"chars": 111765,
"preview": "using MediaBrowser.Controller.Entities;\nusing MediaBrowser.Controller.Library;\nusing Microsoft.Extensions.Logging;\nusing"
},
{
"path": "Shokofin/API/ShokoIdLookup.cs",
"chars": 6542,
"preview": "using System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.IO;\nusing System.Linq;\nusing Media"
},
{
"path": "Shokofin/Collections/CollectionManager.cs",
"chars": 29620,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/Configuration/AllDescriptionsConfiguration.cs",
"chars": 2772,
"preview": "\nusing DescriptionProvider = Shokofin.Utils.TextUtility.DescriptionProvider;\n\nnamespace Shokofin.Configuration;\n\n/// <su"
},
{
"path": "Shokofin/Configuration/AllImagesConfiguration.cs",
"chars": 2674,
"preview": "\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// All image configurations, with support for per structure type per\n"
},
{
"path": "Shokofin/Configuration/AllTitlesConfiguration.cs",
"chars": 2629,
"preview": "using TitleProvider = Shokofin.Utils.TextUtility.TitleProvider;\n\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Ad"
},
{
"path": "Shokofin/Configuration/DebugConfiguration.cs",
"chars": 3848,
"preview": "using System;\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing System.Xml.Serial"
},
{
"path": "Shokofin/Configuration/DescriptionConfiguration.cs",
"chars": 1368,
"preview": "using System.Collections.Generic;\nusing System.Linq;\n\nusing DescriptionProvider = Shokofin.Utils.TextUtility.Description"
},
{
"path": "Shokofin/Configuration/Enums/ImageLanguageType.cs",
"chars": 595,
"preview": "\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Image language types.\n/// </summary>\npublic enum ImageLanguageType"
},
{
"path": "Shokofin/Configuration/Enums/MetadataRefreshField.cs",
"chars": 1823,
"preview": "using System;\n\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Determines which metadata fields to update.\n/// </su"
},
{
"path": "Shokofin/Configuration/Enums/SeasonMergingBehavior.cs",
"chars": 2764,
"preview": "using System;\n\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Determine how to handle series merging.\n/// </summar"
},
{
"path": "Shokofin/Configuration/Enums/SeriesEpisodeConversion.cs",
"chars": 589,
"preview": "\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Series episode conversion configuration.\n/// </summary>\npublic enu"
},
{
"path": "Shokofin/Configuration/Enums/SeriesStructureType.cs",
"chars": 584,
"preview": "\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Library structure type to use for series.\n/// </summary>\npublic en"
},
{
"path": "Shokofin/Configuration/Enums/VirtualRootLocation.cs",
"chars": 553,
"preview": "\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// The virtual root location.\n/// </summary>\npublic enum VirtualRootL"
},
{
"path": "Shokofin/Configuration/ImageConfiguration.cs",
"chars": 3228,
"preview": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace Shokofin.Configuration;\n\npublic class ImageConfiguration"
},
{
"path": "Shokofin/Configuration/LegacyMediaFolderConfiguration.cs",
"chars": 5247,
"preview": "using System;\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing System.Xml.Serial"
},
{
"path": "Shokofin/Configuration/LibraryConfiguration.cs",
"chars": 4362,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.ComponentModel.DataAnnotations;\nusing System.IO;\nusing Syst"
},
{
"path": "Shokofin/Configuration/MediaFolderConfiguration.cs",
"chars": 2888,
"preview": "using System;\nusing System.Linq;\nusing System.Text.Json.Serialization;\nusing System.Xml.Serialization;\n\nnamespace Shokof"
},
{
"path": "Shokofin/Configuration/MetadataRefreshConfiguration.cs",
"chars": 2519,
"preview": "using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.Configuration;\n\n/"
},
{
"path": "Shokofin/Configuration/Models/LibraryConfigurationChangedEventArgs.cs",
"chars": 497,
"preview": "using System;\nusing System.Collections.Generic;\n\nnamespace Shokofin.Configuration.Models;\n\npublic class LibraryConfigura"
},
{
"path": "Shokofin/Configuration/Models/MediaFolderConfigurationChangedEventArgs.cs",
"chars": 392,
"preview": "using System;\n\nnamespace Shokofin.Configuration.Models;\n\npublic class MediaConfigurationChangedEventArgs(LibraryConfigur"
},
{
"path": "Shokofin/Configuration/PluginConfiguration.cs",
"chars": 31502,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.ComponentModel.DataAnnotations;\nusing System.Linq;\nusing Sy"
},
{
"path": "Shokofin/Configuration/SeriesConfiguration.cs",
"chars": 2620,
"preview": "using System.Text.Json.Serialization;\nusing Shokofin.API.Models;\nusing Shokofin.Utils;\n\nnamespace Shokofin.Configuration"
},
{
"path": "Shokofin/Configuration/Services/MediaFolderConfigurationService.cs",
"chars": 27662,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System"
},
{
"path": "Shokofin/Configuration/Services/SeriesConfigurationService.cs",
"chars": 26765,
"preview": "\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing System.T"
},
{
"path": "Shokofin/Configuration/TitleConfiguration.cs",
"chars": 1198,
"preview": "using System.Collections.Generic;\nusing System.Linq;\n\nusing TitleProvider = Shokofin.Utils.TextUtility.TitleProvider;\n\nn"
},
{
"path": "Shokofin/Configuration/TitlesConfiguration.cs",
"chars": 975,
"preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Titles configuration."
},
{
"path": "Shokofin/Configuration/UserConfiguration.cs",
"chars": 2457,
"preview": "using System;\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Shokofin.Configuration;\n\n/// <summary>\n/// Per use"
},
{
"path": "Shokofin/Events/EventDispatchService.cs",
"chars": 40165,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;"
},
{
"path": "Shokofin/Events/Interfaces/IFileEventArgs.cs",
"chars": 1863,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Shokofin.Events.Interfaces;\n\npublic i"
},
{
"path": "Shokofin/Events/Interfaces/IFileRelocationEventArgs.cs",
"chars": 549,
"preview": "\nnamespace Shokofin.Events.Interfaces;\n\npublic interface IFileRelocationEventArgs : IFileEventArgs {\n /// <summary>\n "
},
{
"path": "Shokofin/Events/Interfaces/IMetadataUpdatedEventArgs.cs",
"chars": 1929,
"preview": "using System.Collections.Generic;\nusing System.Globalization;\nusing Jellyfin.Data.Enums;\n\nnamespace Shokofin.Events.Inte"
},
{
"path": "Shokofin/Events/Interfaces/IReleaseSavedEventArgs.cs",
"chars": 168,
"preview": "\nnamespace Shokofin.Events.Interfaces;\n\npublic interface IReleaseSavedEventArgs {\n /// <summary>\n /// Shoko file i"
},
{
"path": "Shokofin/Events/Interfaces/ProviderName.cs",
"chars": 213,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.Events.Interfaces;\n\n[JsonConverter(typeof(JsonStringEnumConver"
},
{
"path": "Shokofin/Events/Interfaces/UpdateReason.cs",
"chars": 1173,
"preview": "using System.Text.Json.Serialization;\n\nnamespace Shokofin.Events.Interfaces;\n\n[JsonConverter(typeof(JsonStringEnumConver"
},
{
"path": "Shokofin/Events/MetadataRefreshService.cs",
"chars": 31061,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/Events/Stub/FileEventArgsStub.cs",
"chars": 1669,
"preview": "using System.Collections.Generic;\nusing System.Linq;\nusing Shokofin.API.Models;\nusing Shokofin.Events.Interfaces;\n\nnames"
},
{
"path": "Shokofin/Extensions/CollectionTypeExtensions.cs",
"chars": 846,
"preview": "\nusing Jellyfin.Data.Enums;\nusing MediaBrowser.Model.Entities;\n\nnamespace Shokofin.Extensions;\n\npublic static class Coll"
},
{
"path": "Shokofin/Extensions/EnumerableExtensions.cs",
"chars": 1092,
"preview": "using System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\n\nnamespace Shokofin.Extensio"
},
{
"path": "Shokofin/Extensions/EpisodeTypeExtensions.cs",
"chars": 479,
"preview": "\nusing Shokofin.API.Models;\n\nnamespace Shokofin.Extensions;\n\npublic static class EpisodeTypeExtensions {\n public stat"
},
{
"path": "Shokofin/Extensions/ListExtensions.cs",
"chars": 735,
"preview": "using System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\n\nnamespace Shokofin.Extensions;\n\npublic static "
},
{
"path": "Shokofin/Extensions/MediaFolderConfigurationExtensions.cs",
"chars": 1587,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing MediaBrowser.Controller.Entities;\nusing Shokofi"
},
{
"path": "Shokofin/Extensions/StringExtensions.cs",
"chars": 15104,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.IO;\nusing System.Lin"
},
{
"path": "Shokofin/Extensions/SyncExtensions.cs",
"chars": 4110,
"preview": "\nusing System;\nusing MediaBrowser.Controller.Entities;\nusing Shokofin.API.Models;\n\nnamespace Shokofin.Extensions;\n\npubli"
},
{
"path": "Shokofin/ExternalIds/AnidbAnimeId.cs",
"chars": 627,
"preview": "using MediaBrowser.Controller.Entities.TV;\nusing MediaBrowser.Controller.Providers;\nusing MediaBrowser.Model.Entities;\nu"
},
{
"path": "Shokofin/ExternalIds/AnidbCreatorId.cs",
"chars": 616,
"preview": "using MediaBrowser.Controller.Entities;\nusing MediaBrowser.Controller.Providers;\nusing MediaBrowser.Model.Entities;\nusin"
},
{
"path": "Shokofin/ExternalIds/AnidbEpisodeId.cs",
"chars": 621,
"preview": "using MediaBrowser.Controller.Entities.TV;\nusing MediaBrowser.Controller.Providers;\nusing MediaBrowser.Model.Entities;\nu"
},
{
"path": "Shokofin/ExternalIds/ProviderNames.cs",
"chars": 590,
"preview": "\nnamespace Shokofin.ExternalIds;\n\npublic struct ProviderNames {\n public const string Anidb = \"AniDB\";\n\n public con"
},
{
"path": "Shokofin/ExternalIds/ProviderUrls.cs",
"chars": 239,
"preview": "\nnamespace Shokofin.ExternalIds;\n\npublic struct ProviderUrls {\n public const string Anidb = \"https://anidb.net\";\n\n "
},
{
"path": "Shokofin/ExternalIds/ShokoExternalUrlHandler.cs",
"chars": 12513,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing S"
},
{
"path": "Shokofin/ExternalIds/ShokoInternalId.cs",
"chars": 943,
"preview": "using MediaBrowser.Controller.Entities;\nusing MediaBrowser.Controller.Entities.Movies;\nusing MediaBrowser.Controller.Ent"
},
{
"path": "Shokofin/MergeVersions/MergeVersionManager.cs",
"chars": 28187,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalys"
},
{
"path": "Shokofin/MergeVersions/MergeVersionSortSelector.cs",
"chars": 1296,
"preview": "\nnamespace Shokofin.MergeVersions;\n\n/// <summary>\n/// Defines how versions of the same video are sorted when merged in t"
},
{
"path": "Shokofin/Pages/Dummy.html",
"chars": 318,
"preview": "<div data-role=\"page\" class=\"page type-interior pluginConfigurationPage withTabs\" data-require=\"emby-input,emby-button,e"
},
{
"path": "Shokofin/Pages/Scripts/Common.js",
"chars": 46745,
"preview": "/**\n * Example page showcasing the different view events we can use and their\n * details.\n */\n\n//#region Dashboard\n\n/**\n"
},
{
"path": "Shokofin/Pages/Scripts/Dummy.js",
"chars": 1328,
"preview": "export default function (view) {\nlet show = false;\nlet hide = false;\nview.addEventListener(\"viewshow\", () => show = true"
},
{
"path": "Shokofin/Pages/Scripts/Settings.js",
"chars": 82651,
"preview": "export default function (view) {\nlet show = false;\nlet hide = false;\nview.addEventListener(\"viewshow\", () => show = true"
},
{
"path": "Shokofin/Pages/Scripts/jsconfig.json",
"chars": 444,
"preview": "{\n \"include\": [\"**/*\"],\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n "
},
{
"path": "Shokofin/Pages/Settings.html",
"chars": 215879,
"preview": "<div data-role=\"page\" class=\"page type-interior pluginConfigurationPage withTabs\" data-require=\"emby-input,emby-button,e"
},
{
"path": "Shokofin/Plugin.cs",
"chars": 18795,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.InteropServices"
},
{
"path": "Shokofin/PluginServiceRegistrator.cs",
"chars": 1702,
"preview": "using MediaBrowser.Controller;\nusing MediaBrowser.Controller.Plugins;\nusing Microsoft.Extensions.DependencyInjection;\n\nn"
},
{
"path": "Shokofin/Providers/BoxSetProvider.cs",
"chars": 4974,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing "
},
{
"path": "Shokofin/Providers/CustomBoxSetProvider.cs",
"chars": 8030,
"preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Jellyfin.D"
},
{
"path": "Shokofin/Providers/CustomEpisodeProvider.cs",
"chars": 8044,
"preview": "using System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser.Controller.Entities;\nusing M"
},
{
"path": "Shokofin/Providers/CustomMovieProvider.cs",
"chars": 4389,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser.Controller.Entities;\nusing MediaBrowser.Control"
},
{
"path": "Shokofin/Providers/CustomSeasonProvider.cs",
"chars": 14781,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/Providers/CustomSeriesProvider.cs",
"chars": 14726,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/Providers/EpisodeProvider.cs",
"chars": 13898,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing "
},
{
"path": "Shokofin/Providers/ImageProvider.cs",
"chars": 7575,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Ta"
},
{
"path": "Shokofin/Providers/MovieProvider.cs",
"chars": 4782,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Ta"
},
{
"path": "Shokofin/Providers/SeasonProvider.cs",
"chars": 7954,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing "
},
{
"path": "Shokofin/Providers/SeriesProvider.cs",
"chars": 5413,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System."
},
{
"path": "Shokofin/Providers/TrailerProvider.cs",
"chars": 3959,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading;\nusing Sy"
},
{
"path": "Shokofin/Providers/VideoProvider.cs",
"chars": 4078,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading;\nusing Sy"
},
{
"path": "Shokofin/Resolvers/Models/LinkGenerationResult.cs",
"chars": 4563,
"preview": "\nusing System;\nusing System.Collections.Concurrent;\nusing Microsoft.Extensions.Logging;\n\nnamespace Shokofin.Resolvers.Mo"
},
{
"path": "Shokofin/Resolvers/Models/ShokoWatcher.cs",
"chars": 381,
"preview": "\nusing System;\nusing System.IO;\nusing Shokofin.Configuration;\n\nnamespace Shokofin.Resolvers.Models;\n\npublic class ShokoW"
},
{
"path": "Shokofin/Resolvers/ShokoIgnoreRule.cs",
"chars": 10643,
"preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Emby.Naming.Common;\nusing Jellyfin"
},
{
"path": "Shokofin/Resolvers/ShokoLibraryMonitor.cs",
"chars": 15951,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing Sys"
},
{
"path": "Shokofin/Resolvers/ShokoResolver.cs",
"chars": 15486,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;"
},
{
"path": "Shokofin/Resolvers/VirtualFileSystemService.cs",
"chars": 96648,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalys"
},
{
"path": "Shokofin/Shokofin.csproj",
"chars": 4995,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <TargetFrameworks>net9.0;net8</TargetFrameworks>\n <"
},
{
"path": "Shokofin/SignalR/Models/EpisodeInfoUpdatedEventArgs.cs",
"chars": 1779,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\nusing Shokofin.API.Co"
},
{
"path": "Shokofin/SignalR/Models/FileEventArgs.cs",
"chars": 2766,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Shokofin.Events.Interfaces;\n\nnamespace Sho"
},
{
"path": "Shokofin/SignalR/Models/FileMovedEventArgs.cs",
"chars": 1574,
"preview": "using System.Text.Json.Serialization;\nusing Shokofin.Events.Interfaces;\n\nnamespace Shokofin.SignalR.Models;\n\npublic clas"
},
{
"path": "Shokofin/SignalR/Models/FileRenamedEventArgs.cs",
"chars": 789,
"preview": "using System.Text.Json.Serialization;\nusing Shokofin.Events.Interfaces;\n\nnamespace Shokofin.SignalR.Models;\n\npublic clas"
},
{
"path": "Shokofin/SignalR/Models/MovieInfoUpdatedEventArgs.cs",
"chars": 1771,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\nusing Shokofin.API.Co"
},
{
"path": "Shokofin/SignalR/Models/ReleaseSavedEventArgs.cs",
"chars": 278,
"preview": "using System.Text.Json.Serialization;\nusing Shokofin.Events.Interfaces;\n\nnamespace Shokofin.SignalR.Models;\n\npublic clas"
},
{
"path": "Shokofin/SignalR/Models/SeriesInfoUpdatedEventArgs.cs",
"chars": 1386,
"preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Jellyfin.Data.Enums;\nusing Shokofin.API.Co"
},
{
"path": "Shokofin/SignalR/SignalRConnectionManager.cs",
"chars": 15226,
"preview": "using System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Net.Sockets;\nusing System.Threading.Tasks;\nusing Je"
},
{
"path": "Shokofin/SignalR/SignalREntryPoint.cs",
"chars": 566,
"preview": "\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Shokofin.SignalR;\n"
},
{
"path": "Shokofin/SignalR/SignalRRetryPolicy.cs",
"chars": 443,
"preview": "using System;\nusing Microsoft.AspNetCore.SignalR.Client;\n\nnamespace Shokofin.SignalR;\n\npublic class SignalrRetryPolicy(T"
},
{
"path": "Shokofin/Sync/SyncDirection.cs",
"chars": 525,
"preview": "using System;\n\nnamespace Shokofin.Sync;\n\n/// <summary>\n/// Determines if we should push or pull the data.\n/// /// </summ"
},
{
"path": "Shokofin/Sync/UserDataSyncManager.cs",
"chars": 36000,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalys"
},
{
"path": "Shokofin/Tasks/AutoRefreshMetadataTask.cs",
"chars": 1601,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/CleanupVirtualRootTask.cs",
"chars": 5759,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System"
},
{
"path": "Shokofin/Tasks/ClearPluginCacheTask.cs",
"chars": 1735,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/ExportUserDataTask.cs",
"chars": 1097,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/ImportUserDataTask.cs",
"chars": 1095,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/MergeEpisodesTask.cs",
"chars": 1577,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/MergeMoviesTask.cs",
"chars": 1573,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/PostScanTask.cs",
"chars": 757,
"preview": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser.Controller.Library;\nusing MediaBr"
},
{
"path": "Shokofin/Tasks/ReconstructCollectionsTask.cs",
"chars": 1592,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/SplitEpisodesTask.cs",
"chars": 1703,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/SplitMoviesTask.cs",
"chars": 1691,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/SyncUserDataTask.cs",
"chars": 1202,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MediaBrowser"
},
{
"path": "Shokofin/Tasks/VersionCheckTask.cs",
"chars": 5299,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/Utils/ContentRating.cs",
"chars": 15862,
"preview": "\nusing System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Diagnostics.CodeAnalysis;\nusi"
},
{
"path": "Shokofin/Utils/DisposableAction.cs",
"chars": 286,
"preview": "\nusing System;\n\nnamespace Shokofin.Utils;\n\npublic class DisposableAction : IDisposable {\n private readonly Action Dis"
},
{
"path": "Shokofin/Utils/GuardedMemoryCache.cs",
"chars": 16823,
"preview": "using System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing AsyncKe"
},
{
"path": "Shokofin/Utils/IgnorePatterns.cs",
"chars": 3410,
"preview": "using System;\nusing DotNet.Globbing;\n\nnamespace Shokofin.Utils;\n\n/// <summary>\n/// Glob patterns for files to ignore.\n//"
},
{
"path": "Shokofin/Utils/ImageUtility.cs",
"chars": 15210,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
},
{
"path": "Shokofin/Utils/LibraryScanWatcher.cs",
"chars": 1311,
"preview": "using System;\nusing MediaBrowser.Controller.Library;\n\nnamespace Shokofin.Utils;\n\npublic class LibraryScanWatcher {\n p"
},
{
"path": "Shokofin/Utils/Ordering.cs",
"chars": 12197,
"preview": "using System;\nusing System.Linq;\nusing Jellyfin.Extensions;\nusing Shokofin.API.Info;\nusing Shokofin.API.Models;\nusing Sh"
},
{
"path": "Shokofin/Utils/PropertyWatcher.cs",
"chars": 1110,
"preview": "using System;\nusing System.Threading.Tasks;\n\nnamespace Shokofin.Utils;\n\npublic class PropertyWatcher<T> {\n private re"
},
{
"path": "Shokofin/Utils/SeriesInfoRelationComparer.cs",
"chars": 4334,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Shokofin.API.Info;\nusing Shokofin.API.Models;\n\n"
},
{
"path": "Shokofin/Utils/TagFilter.cs",
"chars": 23189,
"preview": "\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json.Serialization;\nusing Shokofin"
},
{
"path": "Shokofin/Utils/TextUtility.cs",
"chars": 35773,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Runtime.CompilerServices;\nusing System.T"
},
{
"path": "Shokofin/Utils/UsageTracker.cs",
"chars": 3013,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Timers;\nusing Microsoft.Extensions.Logging;\n\nnamespace S"
},
{
"path": "Shokofin/Web/ImageHostUrl.cs",
"chars": 2959,
"preview": "using System;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Mvc.Filters"
},
{
"path": "Shokofin/Web/Models/SimpleSeries.cs",
"chars": 558,
"preview": "\nnamespace Shokofin.Web.Models;\n\n/// <summary>\n/// A simple series model.\n/// </summary>\npublic class SimpleSeries {\n "
},
{
"path": "Shokofin/Web/Models/VfsLibraryPreview.cs",
"chars": 2467,
"preview": "using System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing MediaBrowser.Model.Entities;\nusing Shokofin."
},
{
"path": "Shokofin/Web/ShokofinHostController.cs",
"chars": 4758,
"preview": "using System;\nusing System.ComponentModel.DataAnnotations;\nusing System.Net.Http;\nusing System.Net.Mime;\nusing System.Te"
},
{
"path": "Shokofin/Web/ShokofinSignalRController.cs",
"chars": 2755,
"preview": "using System;\nusing System.Net.Mime;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Microsoft"
},
{
"path": "Shokofin/Web/ShokofinUtilityController.cs",
"chars": 10314,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.ComponentModel.DataAnnotations;\nusing System.Linq;\nusing Sy"
},
{
"path": "Shokofin/Web/VfsActionFilter.cs",
"chars": 1840,
"preview": "using System.IO;\nusing System.Threading.Tasks;\nusing MediaBrowser.Model.Dto;\nusing Microsoft.AspNetCore.Mvc;\nusing Micro"
},
{
"path": "Shokofin.sln",
"chars": 1104,
"preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.10.350"
},
{
"path": "build.yaml",
"chars": 830,
"preview": "name: \"Shoko\"\nguid: \"5216ccbf-d24a-4eb3-8a7e-7da4230b7052\"\nimageUrl: https://raw.githubusercontent.com/ShokoAnime/Shokof"
},
{
"path": "build_plugin.py",
"chars": 5322,
"preview": "from datetime import datetime\nimport os\nimport json\nimport yaml\nimport argparse\nimport re\n\ndef extract_target_framework"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the ShokoAnime/Shokofin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 201 files (1.5 MB), approximately 326.3k tokens, and a symbol index with 992 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.