diff --git a/CHANGELOG.md b/CHANGELOG.md
index 934cf6a..3bb4267 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,44 @@
# Changelog
+## v1.1.1 — Stable
+
+- When “Save all stills” is enabled but the selected source has no usable Sample Images, saving now skips `extrafanart` automatically instead of interrupting the entire operation.
+- The completion status and local log explicitly report that the still-image output was skipped.
+
+## v1.1.0 — Stable
+
+- Promotes RC1 without functional changes after the complete automated gate and final Windows acceptance passed.
+- Adds an explicit faster cross-volume transfer mode while retaining full target-side SHA-256 verification as the safe default.
+- Makes successful online searches select their new artwork by default while preserving local images as selectable candidates.
+- Accepts a single-movie ID folder through drag and drop, with non-recursive and ambiguity-blocking input rules.
+
+## v1.1.0-rc1 — Release candidate
+
+- Freezes the accepted dev1–dev3 behavior without additional functional changes.
+- Carries forward optional cross-volume verification, post-search online artwork defaults, and single-movie ID-folder drag and drop.
+- Requires the complete offline automated gate and a final compact Windows acceptance pass before stable promotion.
+
+## v1.1.0-dev3 — ID folder drag and drop
+
+- Accepts either one supported movie file or one single-movie ID folder through drag and drop.
+- Resolves only a folder's top-level movie file so metadata subfolders and unrelated nested media are never scanned.
+- Rejects empty or multi-movie folders with a clear localized message instead of guessing which movie to open.
+
+## v1.1.0-dev2 — Online artwork becomes the post-search default
+
+- Switches poster and fanart to the preferred successful online source after a search, matching the existing metadata-field behavior.
+- Keeps previously loaded local images and a manually chosen cover in the artwork source menu so the user can switch back explicitly.
+- Leaves initial movie loading unchanged: local sidecar images remain the default until a successful search supplies new artwork.
+
+## v1.1.0-dev1 — Optional cross-volume verification
+
+- Keeps full target-side SHA-256 verification as the safe default for cross-volume and UNC transfers.
+- Adds an explicit fast-transfer option that skips the second complete target read and checks copy completion plus file size only.
+- Preserves target-side staging, late-conflict protection, cancellation, source retirement after commit, and rollback in both modes.
+- Shows the selected transfer policy in the target hint and save preview, with a clear at-your-own-risk warning for fast mode.
+- Persists the choice only when the user enables remembered save preferences and migrates schema-v4 settings to full verification.
+- Adds four-language UI copy plus offline preference, transaction, preview, and UI regression coverage.
+
## v1.0.0 — First public stable release
- Establishes the accepted v0.9.0 feature set as the first public, feature-frozen release.
diff --git a/JavMetaLite.App/JavMetaLite.App.csproj b/JavMetaLite.App/JavMetaLite.App.csproj
index ae979df..8ba3735 100644
--- a/JavMetaLite.App/JavMetaLite.App.csproj
+++ b/JavMetaLite.App/JavMetaLite.App.csproj
@@ -7,9 +7,9 @@
enable
JavMetaLite
JavMetaLite.App
- 1.0.0
- 1.0.0.0
- 1.0.0
+ 1.1.1
+ 1.1.1.0
+ 1.1.1
false
Noredge
Noredge
diff --git a/JavMetaLite.App/MainWindow.xaml b/JavMetaLite.App/MainWindow.xaml
index f546f5a..c782fff 100644
--- a/JavMetaLite.App/MainWindow.xaml
+++ b/JavMetaLite.App/MainWindow.xaml
@@ -297,7 +297,7 @@
-
+
@@ -479,7 +479,7 @@
+ ToolTip="{DynamicResource Main.RememberPreferencesTooltipVerification}" />
@@ -493,6 +493,13 @@
+
@@ -550,7 +557,7 @@
-
+
-
+
diff --git a/JavMetaLite.App/MainWindow.xaml.cs b/JavMetaLite.App/MainWindow.xaml.cs
index efbbb97..a3ed8ad 100644
--- a/JavMetaLite.App/MainWindow.xaml.cs
+++ b/JavMetaLite.App/MainWindow.xaml.cs
@@ -75,7 +75,7 @@ internal MainWindow(AppPreferencesStore preferencesStore)
_uiInitialized = true;
ApplyMetadata(_metadata, []);
RefreshTargetLocationUi();
- AppLog.Info("JavMetaLite v1.0.0 启动");
+ AppLog.Info("JavMetaLite v1.1.1 启动");
}
internal void LoadPreferences()
@@ -110,6 +110,7 @@ internal void LoadPreferences()
AppLog.Info(
$"已恢复保存偏好 target={result.Preferences.TargetMode} " +
$"rename={result.Preferences.RenameVideo} directOverwrite={result.Preferences.DirectSaveOverwrite} " +
+ $"crossVolumeVerification={result.Preferences.CrossVolumeVerification} " +
$"customRoot={result.Preferences.CustomRootDirectory}");
SetStatus(
result.Preferences.DirectSaveOverwrite
@@ -123,6 +124,8 @@ private void ApplyPreferences(AppPreferences preferences)
{
ApplyLanguagePreference(preferences.UiLanguage);
DirectSaveOverwriteCheckBox.IsChecked = preferences.DirectSaveOverwrite;
+ SkipCrossVolumeVerificationCheckBox.IsChecked =
+ preferences.CrossVolumeVerification is CrossVolumeVerificationMode.FileSizeOnly;
RememberPreferencesCheckBox.IsChecked = preferences.RememberSavePreferences;
WriteNfoCheckBox.IsChecked = preferences.WriteNfo;
DownloadPosterCheckBox.IsChecked = preferences.DownloadPoster;
@@ -158,6 +161,7 @@ private AppPreferences CapturePreferences()
UiLanguage = LocalizationService.CurrentLanguageCode,
RememberSavePreferences = RememberPreferencesCheckBox.IsChecked == true,
DirectSaveOverwrite = DirectSaveOverwriteCheckBox.IsChecked == true,
+ CrossVolumeVerification = GetCrossVolumeVerificationMode(),
TargetMode = GetSelectedTargetMode(),
CustomRootDirectory = string.IsNullOrWhiteSpace(CustomRootTextBox.Text)
? _lastValidCustomRootDirectory
@@ -269,15 +273,49 @@ private async void ChooseFile_Click(object sender, RoutedEventArgs e)
private void Window_DragOver(object sender, DragEventArgs e)
{
- e.Effects = TryGetDroppedVideo(e.Data, out _) ? DragDropEffects.Copy : DragDropEffects.None;
+ e.Effects = TryGetSingleDroppedPath(e.Data, out var path) &&
+ (VideoFileSupport.IsSupportedExistingFile(path) || Directory.Exists(path))
+ ? DragDropEffects.Copy
+ : DragDropEffects.None;
e.Handled = true;
}
private async void Window_Drop(object sender, DragEventArgs e)
{
- if (TryGetDroppedVideo(e.Data, out var path))
+ if (!TryGetSingleDroppedPath(e.Data, out var path))
{
- await SelectVideoAsync(path!);
+ return;
+ }
+
+ try
+ {
+ var resolution = VideoFileSupport.ResolveInputPath(path);
+ if (resolution.Success)
+ {
+ if (Directory.Exists(path))
+ {
+ AppLog.Info($"从拖入番号文件夹解析影片 folder={path} video={resolution.VideoPath}");
+ }
+ await SelectVideoAsync(resolution.VideoPath!);
+ }
+ else
+ {
+ ShowError(LocalizationService.Get(resolution.Status switch
+ {
+ VideoInputPathStatus.FolderHasNoVideo => "Error.FolderHasNoVideo",
+ VideoInputPathStatus.FolderHasMultipleVideos => "Error.FolderHasMultipleVideos",
+ _ => "Error.UnsupportedVideo"
+ }));
+ }
+ }
+ catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
+ {
+ AppLog.Warning($"无法读取拖入的影片文件夹 path={path}", exception);
+ ShowError(LocalizationService.Get("Error.ReadVideoFolder", exception.Message));
+ }
+ finally
+ {
+ e.Handled = true;
}
}
@@ -500,6 +538,11 @@ await RunBusyAsync(LocalizationService.Get("Status.Saving"), async () =>
? LocalizationService.Get("Status.SidecarsMigrated")
: LocalizationService.Get("Status.NoChanges")
: string.Join(LocalizationService.Get("Common.ListSeparator"), outputs);
+ if (options.DownloadExtrafanart && result.Outputs.ExtrafanartPaths.Count == 0)
+ {
+ outputSummary += LocalizationService.Get("Common.ListSeparator") +
+ LocalizationService.Get("Status.ExtrafanartSkipped");
+ }
SetStatus(LocalizationService.Get("Status.SaveComplete", outputSummary, fanartNote, moveNote), true);
});
}
@@ -746,7 +789,13 @@ private OrganizationOptions GetOrganizationOptions() =>
RenameVideoCheckBox.IsChecked == true,
GetSelectedTargetMode() is OrganizationTargetMode.CustomRootNumberFolder
? CustomRootTextBox.Text
- : null);
+ : null,
+ GetCrossVolumeVerificationMode());
+
+ private CrossVolumeVerificationMode GetCrossVolumeVerificationMode() =>
+ SkipCrossVolumeVerificationCheckBox.IsChecked == true
+ ? CrossVolumeVerificationMode.FileSizeOnly
+ : CrossVolumeVerificationMode.FullSha256;
private OrganizationTargetMode GetSelectedTargetMode()
{
@@ -771,6 +820,7 @@ private void RefreshTargetLocationUi()
private void RefreshTargetLocationPreview()
{
_targetConfigurationError = null;
+ SkipCrossVolumeVerificationCheckBox.Visibility = Visibility.Collapsed;
var customMode = GetSelectedTargetMode() is OrganizationTargetMode.CustomRootNumberFolder;
if (customMode &&
!string.IsNullOrWhiteSpace(CustomRootTextBox.Text) &&
@@ -808,8 +858,14 @@ private void RefreshTargetLocationPreview()
TargetPathHintText.Text = LocalizationService.Get("Main.FinalVideo", pathPlan.TargetVideoPath);
if (pathPlan.RequiresVerifiedCopy)
{
- TargetPathHintText.Text += Environment.NewLine + LocalizationService.Get("Main.SafeCopy");
- TargetPathHintText.Foreground = new SolidColorBrush(Color.FromRgb(141, 184, 255));
+ SkipCrossVolumeVerificationCheckBox.Visibility = Visibility.Visible;
+ var fastCopy = GetCrossVolumeVerificationMode() is CrossVolumeVerificationMode.FileSizeOnly;
+ TargetPathHintText.Text += Environment.NewLine + LocalizationService.Get(
+ fastCopy ? "Main.FastCopy" : "Main.SafeCopy");
+ TargetPathHintText.Foreground = new SolidColorBrush(
+ fastCopy
+ ? Color.FromRgb(255, 209, 138)
+ : Color.FromRgb(141, 184, 255));
}
else
{
@@ -1036,6 +1092,9 @@ private MovieMetadata ApplyOnlineSources(
IReadOnlyList onlineSources)
{
var retainedManualCandidates = CaptureManualCandidates();
+ _preferredArtworkSourceName = MetadataCandidateSource
+ .FromMetadata(preferredOnlineMetadata)
+ .Name;
if (_localSourceMetadata is null)
{
ApplyMetadataCore(preferredOnlineMetadata, onlineSources, retainedManualCandidates);
@@ -1725,6 +1784,7 @@ private static string GetLocalizedTransactionProgress(FileTransactionProgress up
FileTransactionStage.VerifyingMovie => LocalizationService.Get("Progress.Verifying"),
FileTransactionStage.Committing => LocalizationService.Get("Progress.Committing"),
FileTransactionStage.RetiringSource => LocalizationService.Get("Progress.RetiringSource"),
+ FileTransactionStage.RetiringSourceFast => LocalizationService.Get("Progress.RetiringSourceFast"),
FileTransactionStage.Completed => LocalizationService.Get("Progress.Completed"),
_ => update.Message
};
@@ -1834,7 +1894,7 @@ private void OpenLogs_Click(object sender, RoutedEventArgs e)
}
}
- private static bool TryGetDroppedVideo(IDataObject data, out string? path)
+ private static bool TryGetSingleDroppedPath(IDataObject data, out string? path)
{
path = null;
if (!data.GetDataPresent(DataFormats.FileDrop) || data.GetData(DataFormats.FileDrop) is not string[] files || files.Length != 1)
@@ -1843,7 +1903,7 @@ private static bool TryGetDroppedVideo(IDataObject data, out string? path)
}
path = files[0];
- return VideoFileSupport.IsSupportedExistingFile(path);
+ return true;
}
private void Window_Closing(object? sender, CancelEventArgs e)
diff --git a/JavMetaLite.App/Resources/Strings.en.xaml b/JavMetaLite.App/Resources/Strings.en.xaml
index 3814458..100fd7c 100644
--- a/JavMetaLite.App/Resources/Strings.en.xaml
+++ b/JavMetaLite.App/Resources/Strings.en.xaml
@@ -3,13 +3,13 @@
JavMetaLite.LocalizationJAV Metadata LiteCancelSaveConfirm and runOpen logsChoose folder(blank); ,
Single movieEdit one movie, review every change, and choose where the movie and metadata are savedDisplay languageChoose movieCover / FanartDrop one movie file hereNo movie selectedThe movie will remain in its current locationIDSearchImport pageRead a JAVLibrary details pageMulti-source searchSearch LibreDMM and R18.dev together (recommended)Manual entry only
TitleOriginal titleRelease dateRuntime (minutes)RuntimeStudioDirectorLabelSeriesActors (comma-separated)ActorsGenres (comma-separated)GenresPlotRating
- OutputCreate NFOHD posterCrop a portrait poster from the high-resolution sourceLandscape fanartUse the complete landscape coverSave all stillsSave Sample Images in the extrafanart folderSave modeSave and overwrite directly (skip preview)Skip the pre-save preview and overwrite existing NFO or image files. The movie file is never overwritten.Rename movie to its IDChange only the movie filename, not its extensionRemember save preferencesRemember the save mode, target location, and output options on this row, including direct overwrite
- Target locationMovie location (do not move)ID folder at source locationID folder under a custom rootCustom rootPaste a local absolute path or UNC network share pathRecent foldersRecent folders ({0}) ▾Choose or manage recently used custom rootsSelect a movie to show the final pathChoose a custom target root; the final path appears after selecting a movieFinal movie: {0}Transfer: safe copy + SHA-256 verification, then remove the sourceCancel current operationReady. Choose or drop one movieDisplay language changedv1.0.0 · Stable
+ OutputCreate NFOHD posterCrop a portrait poster from the high-resolution sourceLandscape fanartUse the complete landscape coverSave all stillsSave Sample Images in the extrafanart folderSave modeSave and overwrite directly (skip preview)Skip the pre-save preview and overwrite existing NFO or image files. The movie file is never overwritten.Rename movie to its IDChange only the movie filename, not its extensionRemember save preferencesRemember the save mode, target location, cross-volume verification, and output options, including direct overwriteSkip cross-volume SHA-256 verification (faster, at your own risk)Check only copy completion and file size; movie contents are not verified byte for byte
+ Target locationMovie location (do not move)ID folder at source locationID folder under a custom rootCustom rootPaste a local absolute path or UNC network share pathRecent foldersRecent folders ({0}) ▾Choose or manage recently used custom rootsSelect a movie to show the final pathChoose a custom target root; the final path appears after selecting a movieFinal movie: {0}Transfer: safe copy + SHA-256 verification, then remove the sourceTransfer: fast cross-volume copy; file-size check only (at your own risk)Cancel current operationReady. Choose or drop one movieDisplay language changedv1.1.1 · Stable
JAVLibrary browserComplete verification and open the correct movie details pageRead current movieOnly movie data from the current page is imported. The embedded browser may retain cookies used for site verification.Could not start the built-in browser: {0}
Make sure Microsoft Edge WebView2 Runtime is installed.This is not a movie details page. Open the correct movie in the page, then click Read.
Review changes before savingMetadata is written or the movie is moved only after you click “Confirm and run.”Current movieFinal movieI confirm overwriting the existing metadata files listed aboveSaving is blocked when the movie target conflicts.Cancel, resolve the target movie conflict, and preview again.{0} existing metadata file(s) found. Overwrite is enabled; confirmation will replace them.{0} existing metadata file(s) found. Explicit confirmation is required. The movie file is never overwritten.
- Create folderMove movieRename movieMove and renameCopy and verifyUpdateKeep unchangedReplace imageOverwriteGenerateConflict
+ Create folderMove movieRename movieMove and renameCopy and verifyFast copyUpdateKeep unchangedReplace imageOverwriteGenerateConflict
Choose a movieChoose media library rootChoose a complete coverImage files|*.jpg;*.jpeg;*.png;*.webp|All files|*.*Remove current entryClear recent entriesChoose local complete cover…Choose one image to generate both poster and fanartSupports JPG, JPEG, PNG, and WEBP
- Restored save preferences: direct overwrite is enabledRestored the save preferences you chose to rememberManual mode is active. Enter metadata and save directlyFetching LibreDMM and R18.dev…Fetching movie metadata…; {0} returned no result, {0} still image(s) found, no separate still imagesLoaded {1} from {0}Loaded new data from {0}; each field can switch back to the local NFO; fanart preview was not loaded; cover preview was not loaded, but metadata remains editableBrowser verification is required. Opening the built-in browser…Canceled. The movie and metadata were not changedSafely generating and committing files…extrafanart ({0}); fanart came from the complete cover; movie organized as {0}sidecars safely migratedno changes need to be writtenSave complete: {0}{1}{2}Checking local metadata beside the movie…
+ Restored save preferences: direct overwrite is enabledRestored the save preferences you chose to rememberManual mode is active. Enter metadata and save directlyFetching LibreDMM and R18.dev…Fetching movie metadata…; {0} returned no result, {0} still image(s) found, no separate still imagesLoaded {1} from {0}Loaded new data from {0}; each field can switch back to the local NFO; fanart preview was not loaded; cover preview was not loaded, but metadata remains editableBrowser verification is required. Opening the built-in browser…Canceled. The movie and metadata were not changedSafely generating and committing files…extrafanart ({0}); fanart came from the complete cover; movie organized as {0}sidecars safely migratedno changes need to be writtenno still images found; skipped automaticallySave complete: {0}{1}{2}Checking local metadata beside the movie…
Removed this folder from history; the current path is unchangedCleared recent target roots; the current path is unchangedThe custom target root is unavailable: {0}. Reconnect it or choose another folder; the app will not create the root automatically.The local NFO cannot be read safely; repair or move it, then select the movie againSaving updates managed fields and preserves detected unknown XMLSaving updates managed fields onlyCould not inspect local files beside the movie: {0}Recognized ID {0}; no matching local NFO was found, so you can search onlineNo matching local NFO or ID was found in the filename; enter the ID manuallyLoaded local NFO (safe to update): {0}{1}A local NFO was found but could not be read safely. The original was not changed: {0}; {1}
Ignored {0} invalid local image(s) (see logs); ignored {0} invalid image(s); preview loading is incompleteLocal images loaded ({0}){1}{2}poster only; fanart missingfanart only; poster missingno usable imagesReading metadata from the browser…; each field can switch back to the local NFOLoaded new browser metadata for {0}{1}Changed “{0}” to {1}Loading {0} cover…Cover and fanart both changed to {0}The {0} cover preview is incomplete; choose another source if neededReading local complete cover…Selected local complete cover: {0}; poster and fanart will use the same sourceOperation canceled; unfinished file transactions were safely restoredCanceling and restoring files. Please wait…Log folder: {0}
Enter a movie ID first.Choose a movie file first.The local NFO cannot be read safely. To protect the original, repair or move that NFO and select the movie again before saving.Choose a supported movie file.Could not open the log folder: {0}No readable movie path was provided at startup.Could not read the movie path provided at startup.
@@ -17,6 +17,14 @@
Preferences could not be read; safe defaults are in use
Preferences came from a newer version; safe defaults are in use and the original file will not be overwritten
Movie files|*.mp4;*.m4v;*.mkv;*.avi;*.wmv;*.mov;*.webm;*.ts;*.m2ts|All files|*.*
- Create ID folderMove movieRename movieMove and rename movieSafely copy and verify the movie, then remove the sourceGenerate NFOOverwrite NFOUpdate NFOUpdate NFO (preserve unknown XML)Keep NFO content unchangedGenerate {0}Replace {0}Keep {0} unchangedLocal {0} is missing; keep it missingMigrate and keep {0}Generate still imageReplace still imageThe custom target root path is occupied by a file: {0}The custom target root is unavailable and will not be created automatically: {0}The target folder path is occupied by a file: {0}The target movie already exists and will not be overwritten: {0}Preparing metadata and the safe transaction…Copying movie to the target staging area…Copying movie to the target staging area… {0}%Verifying target movie SHA-256…Verifying target movie SHA-256… {0}%Committing the target movie and metadata…Target verified and committed; removing the source movie…Safe save completed
+ Create ID folderMove movieRename movieMove and rename movieSafely copy and verify the movie, then remove the sourceFast-copy the movie; check file size only, without content verification, then remove the sourceGenerate NFOOverwrite NFOUpdate NFOUpdate NFO (preserve unknown XML)Keep NFO content unchangedGenerate {0}Replace {0}Keep {0} unchangedLocal {0} is missing; keep it missingMigrate and keep {0}Generate still imageReplace still imageThe custom target root path is occupied by a file: {0}The custom target root is unavailable and will not be created automatically: {0}The target folder path is occupied by a file: {0}The target movie already exists and will not be overwritten: {0}Preparing metadata and the safe transaction…Copying movie to the target staging area…Copying movie to the target staging area… {0}%Verifying target movie SHA-256…Verifying target movie SHA-256… {0}%Committing the target movie and metadata…Target copied and committed; removing the source movie…Safe save completed
{0} did not respond within {1:0} seconds. Try again later or choose another source.
+ Remember the save mode, target location, cross-volume verification, and output options, including direct overwrite
+ Target copied and committed; removing the source movie…
+ v1.1.1 · Stable
+ Drop one movie file or ID folder
+ Ready. Choose a movie, or drop one movie file or ID folder
+ No supported movie file was found at the top level of this folder.
+ This folder contains more than one supported movie file. Drop one movie file instead.
+ The dropped folder could not be read: {0}
diff --git a/JavMetaLite.App/Resources/Strings.ja.xaml b/JavMetaLite.App/Resources/Strings.ja.xaml
index ea87f85..cbcac72 100644
--- a/JavMetaLite.App/Resources/Strings.ja.xaml
+++ b/JavMetaLite.App/Resources/Strings.ja.xaml
@@ -4,12 +4,12 @@
1作品モード1作品ずつ編集し、すべての変更と動画・メタデータの保存先を確認できます表示言語動画を選択ジャケット / ファンアート動画ファイルを1つドロップ動画が選択されていません動画は現在の場所に残ります品番検索Webから取得JAVLibrary の詳細ページから読み込みます複数ソース検索(推奨)LibreDMM と R18.dev を同時に検索します(推奨)手動入力のみ
タイトル原題発売日収録時間(分)収録時間メーカー監督レーベルシリーズ出演者(カンマ区切り)出演者ジャンル(カンマ区切り)ジャンルあらすじ評価
出力NFO を作成高画質ポスター高解像度のジャケットから縦長ポスターを自動生成します横長ファンアート見開きジャケット全体を使用しますサンプル画像をすべて保存サンプル画像を extrafanart フォルダーに保存します保存方法直接保存して上書き(プレビュー省略)保存前プレビューを省略し、既存の NFO または画像を上書きします。動画ファイルは上書きしません。動画名を品番に変更拡張子を変えず、動画のファイル名だけを変更します保存設定を記憶直接上書きを含む保存方法、保存先、出力オプションを記憶します
- 保存先動画と同じ場所(移動しない)元の場所に品番フォルダーを作成指定ルート下に品番フォルダーを作成指定ルートローカルの絶対パスまたは UNC ネットワーク共有パスを貼り付けられます最近のフォルダー最近のフォルダー ({0}) ▾最近使用したルートを選択または管理します動画を選択すると最終パスが表示されます保存先ルートを選択してください。動画選択後に最終パスが表示されます保存先動画:{0}転送方法:安全なコピー + SHA-256 検証後に元ファイルを削除現在の処理をキャンセル準備完了。動画を選択またはドロップしてください表示言語を変更しましたv1.0.0 · 安定版
+ 保存先動画と同じ場所(移動しない)元の場所に品番フォルダーを作成指定ルート下に品番フォルダーを作成指定ルートローカルの絶対パスまたは UNC ネットワーク共有パスを貼り付けられます最近のフォルダー最近のフォルダー ({0}) ▾最近使用したルートを選択または管理します動画を選択すると最終パスが表示されます保存先ルートを選択してください。動画選択後に最終パスが表示されます保存先動画:{0}転送方法:安全なコピー + SHA-256 検証後に元ファイルを削除現在の処理をキャンセル準備完了。動画を選択またはドロップしてください表示言語を変更しましたv1.1.1 · 安定版
JAVLibrary ブラウザー認証を完了し、正しい作品詳細ページを開いてください現在の作品を読み込む現在のページから作品情報だけを取り込みます。内蔵ブラウザーはサイト認証用の Cookie を保持する場合があります。内蔵ブラウザーを起動できません:{0}
Microsoft Edge WebView2 Runtime がインストールされていることを確認してください。現在のページは作品詳細ページではありません。正しい作品を開いてから読み込んでください。
保存前の変更確認「確認して実行」を押した後にのみメタデータの書き込みまたは動画の移動を行います。現在の動画保存先動画上記の既存メタデータファイルを上書きすることを確認します動画の保存先が競合する場合は保存されません。キャンセルして保存先動画の競合を解消し、もう一度プレビューしてください。既存のメタデータファイルが {0} 件あります。上書きが有効なため、確認後に置き換えます。既存のメタデータファイルが {0} 件あります。上書きには明示的な確認が必要です。動画ファイルは上書きしません。
フォルダー作成動画を移動動画名を変更移動して名前変更コピーして検証更新変更なし画像を置換上書き生成競合
動画を選択メディアライブラリのルートを選択見開きジャケット画像を選択画像ファイル|*.jpg;*.jpeg;*.png;*.webp|すべてのファイル|*.*現在の履歴を削除最近の履歴を消去ローカルの見開きジャケットを選択…1枚の画像からポスターとファンアートを生成しますJPG、JPEG、PNG、WEBP に対応
- 保存設定を復元しました:直接上書きが有効です記憶された保存設定を復元しました手動入力モードです。情報を入力して保存できますLibreDMM と R18.dev から取得中…作品情報を取得中…、{0} から結果を取得できませんでした、サンプル画像 {0} 枚、個別のサンプル画像なし{0} から {1} を読み込みました{0} から新しい情報を読み込みました。各項目はローカル NFO に戻せます、ファンアートのプレビューを読み込めませんでした、ジャケットのプレビューを読み込めませんでしたが、情報は編集できますブラウザー認証が必要です。内蔵ブラウザーを開いています…キャンセルしました。動画とメタデータは変更されていませんファイルを安全に生成して反映中…extrafanart({0} 枚)、ファンアートは見開きジャケットから生成、動画を {0} として整理サイドカーファイルを安全に移行しました書き込む変更はありません保存完了:{0}{1}{2}動画と同じ場所にあるローカルメタデータを確認中…
+ 保存設定を復元しました:直接上書きが有効です記憶された保存設定を復元しました手動入力モードです。情報を入力して保存できますLibreDMM と R18.dev から取得中…作品情報を取得中…、{0} から結果を取得できませんでした、サンプル画像 {0} 枚、個別のサンプル画像なし{0} から {1} を読み込みました{0} から新しい情報を読み込みました。各項目はローカル NFO に戻せます、ファンアートのプレビューを読み込めませんでした、ジャケットのプレビューを読み込めませんでしたが、情報は編集できますブラウザー認証が必要です。内蔵ブラウザーを開いています…キャンセルしました。動画とメタデータは変更されていませんファイルを安全に生成して反映中…extrafanart({0} 枚)、ファンアートは見開きジャケットから生成、動画を {0} として整理サイドカーファイルを安全に移行しました書き込む変更はありませんサンプル画像が見つからなかったため、自動的にスキップしました保存完了:{0}{1}{2}動画と同じ場所にあるローカルメタデータを確認中…
現在のフォルダーを履歴から削除しました。入力中のパスは変更されません最近の保存先を消去しました。入力中のパスは変更されません指定した保存先ルートを利用できません:{0}。再接続するか別のフォルダーを選択してください。ルートは自動作成されません。ローカル NFO を安全に読み込めません。修復または移動してから動画を選び直してください保存時は管理対象の項目だけを更新し、不明な XML を保持します保存時は管理対象の項目だけを更新します動画の横にあるローカルファイルを確認できません:{0}品番 {0} を認識しました。同名のローカル NFO はないため検索できます同名のローカル NFO がなく、ファイル名から品番を認識できません。手動で入力してくださいローカル NFO を読み込みました(安全に更新可能):{0}{1}ローカル NFO を検出しましたが安全に読み込めません。元ファイルは変更していません:{0}、{1}
無効なローカル画像 {0} 件を無視しました(ログ参照)、無効な画像 {0} 件を無視、プレビューの読み込みが不完全ですローカル画像を読み込みました({0}){1}{2}ポスターのみ、ファンアートなしファンアートのみ、ポスターなし利用可能な画像なしブラウザーから情報を読み込み中…。各項目はローカル NFO に戻せますブラウザーから {0} の新しい情報を読み込みました{1}「{0}」を {1} に切り替えました{0} のジャケットを読み込み中…ジャケットとファンアートを {0} に切り替えました{0} のジャケットプレビューが不完全です。必要に応じて別のソースを選択してくださいローカルの見開きジャケットを読み込み中…ローカルの見開きジャケットを選択しました:{0}。ポスターとファンアートは同じソースから生成されます処理をキャンセルし、未完了のファイル操作を安全に復元しましたキャンセルしてファイルを復元中です。しばらくお待ちください…ログフォルダー:{0}
先に品番を入力してください。先に動画ファイルを選択してください。ローカル NFO を安全に読み込めません。元ファイルを保護するため、その NFO を修復または移動して動画を選び直すまで保存できません。対応している動画ファイルを選択してください。ログフォルダーを開けません:{0}起動時に読み込み可能な動画パスが指定されていません。起動時に指定された動画パスを読み込めません。
@@ -19,4 +19,17 @@
動画ファイル|*.mp4;*.m4v;*.mkv;*.avi;*.wmv;*.mov;*.webm;*.ts;*.m2ts|すべてのファイル|*.*
品番フォルダーを作成動画を移動動画名を変更動画を移動して名前を変更動画を安全にコピーして検証後、元ファイルを削除NFO を生成NFO を上書きNFO を更新NFO を更新(不明な XML を保持)NFO の内容を変更しない{0} を生成{0} を置換{0} を変更しないローカル {0} は存在しないため、そのまま保持{0} を維持して移行サンプル画像を生成サンプル画像を置換保存先ルートのパスがファイルによって使用されています:{0}保存先ルートを利用できず、自動作成もされません:{0}保存先フォルダーのパスがファイルによって使用されています:{0}保存先動画が既に存在するため上書きしません:{0}メタデータと安全な処理を準備中…動画を保存先の一時領域へコピー中…動画を保存先の一時領域へコピー中… {0}%保存先動画の SHA-256 を検証中…保存先動画の SHA-256 を検証中… {0}%保存先動画とメタデータを反映中…保存先の検証と反映が完了しました。元動画を削除中…安全な保存が完了しました
{0} が {1:0} 秒以内に応答しませんでした。後でもう一度試すか、別のソースを選択してください。
+ ドライブ間の SHA-256 検証を省略(高速・自己責任)
+ コピー完了とファイルサイズのみを確認し、動画内容の完全一致は検証しません
+ 転送方法:高速なドライブ間コピー(サイズ確認のみ・自己責任)
+ 高速コピー
+ 動画を高速コピーします。サイズのみ確認し、内容は検証せず、成功後に元ファイルを削除します
+ 直接上書きを含む保存方法、保存先、ドライブ間検証、出力オプションを記憶します
+ 保存先へのコピーと反映が完了しました。元動画を削除中…
+ v1.1.1 · 安定版
+ 動画ファイルまたは品番フォルダーを1つドロップ
+ 準備完了。動画を選択するか、動画ファイルまたは品番フォルダーを1つドロップしてください
+ このフォルダーの直下に対応動画ファイルが見つかりません。
+ このフォルダーには対応動画ファイルが複数あります。動画ファイルを1つだけドロップしてください。
+ ドロップしたフォルダーを読み取れません:{0}
diff --git a/JavMetaLite.App/Resources/Strings.zh-Hans.xaml b/JavMetaLite.App/Resources/Strings.zh-Hans.xaml
index 374bb4c..9da5a89 100644
--- a/JavMetaLite.App/Resources/Strings.zh-Hans.xaml
+++ b/JavMetaLite.App/Resources/Strings.zh-Hans.xaml
@@ -57,7 +57,9 @@
影片重命名为番号
只改变影片文件名,不改变扩展名
记住保存偏好
- 记住本行保存方式、目标位置和输出选项,包括直接保存并覆盖
+ 记住本行保存方式、目标位置、跨盘校验和输出选项,包括直接保存并覆盖
+ 跳过跨盘 SHA-256 校验(更快,风险自负)
+ 仅检查复制完成和文件大小,不验证影片内容是否逐字节一致
目标位置
影片所在位置(不移动)
来源位置的番号文件夹
@@ -71,10 +73,11 @@
请选择自定义目标根目录;选择影片后将显示最终路径
最终影片:{0}
传输方式:安全复制 + SHA-256 校验,成功后移除来源
+ 传输方式:快速跨盘复制,仅检查文件大小(风险自负)
取消当前操作
准备就绪,请选择或拖入一个影片
界面语言已切换
- v1.0.0 · 稳定版
+ v1.1.1 · 稳定版
JAVLibrary 浏览器
完成验证并打开正确的影片详情页
@@ -97,6 +100,7 @@
重命名影片
移动并重命名
复制并校验
+ 快速复制
更新
保持不变
替换图片
@@ -108,6 +112,7 @@
重命名影片
移动并重命名影片
安全复制并校验影片,成功后移除来源
+ 快速复制影片;仅检查文件大小,不校验内容,成功后移除来源
生成 NFO
覆盖 NFO
更新 NFO
@@ -130,7 +135,7 @@
正在校验目标影片 SHA-256…
正在校验目标影片 SHA-256… {0}%
正在提交目标影片与 metadata…
- 目标校验与提交完成,正在移除来源影片…
+ 目标复制与提交完成,正在移除来源影片…
安全保存已完成
选择一个影片
@@ -165,6 +170,7 @@
;影片已整理为 {0}
sidecar 已安全迁移
没有需要写入的变更
+ 未找到剧照,已自动跳过
保存完成:{0}{1}{2}
正在检查影片旁的本地 metadata…
已移除当前目录的历史记录;当前路径保持不变
@@ -229,4 +235,12 @@
图片预览下载失败。
JAV Metadata Lite 启动失败。
错误记录:{0}
{1}
资料来源 {0} 在 {1:0} 秒内没有响应,请稍后重试或更换来源。
+ 记住保存方式、目标位置、跨盘校验和输出选项,包括直接保存并覆盖
+ 目标复制与提交完成,正在移除来源影片…
+ v1.1.1 · 稳定版
+ 拖入一个影片文件或番号文件夹
+ 准备就绪,请选择影片,或拖入一个影片文件或番号文件夹
+ 该文件夹顶层没有找到支持的影片文件。
+ 该文件夹包含多个支持的影片文件,请改为拖入单个影片文件。
+ 无法读取拖入的文件夹:{0}
diff --git a/JavMetaLite.App/Resources/Strings.zh-Hant.xaml b/JavMetaLite.App/Resources/Strings.zh-Hant.xaml
index 30102d5..e104968 100644
--- a/JavMetaLite.App/Resources/Strings.zh-Hant.xaml
+++ b/JavMetaLite.App/Resources/Strings.zh-Hant.xaml
@@ -8,12 +8,12 @@
單片模式單片編輯,可檢查全部變更,並可選擇影片與 metadata 的儲存位置介面語言選擇影片封套 / Fanart拖入一個影片檔案尚未選擇影片影片將保留在原位置番號搜尋資料網頁匯入從 JAVLibrary 詳情頁讀取多來源搜尋(建議)同時搜尋 LibreDMM 與 R18.dev(建議)僅手動填寫
標題原始標題發行日期時長(分鐘)時長片商導演標籤 / 廠牌系列演員(逗號分隔)演員類型(逗號分隔)類型簡介評分
輸出產生 NFO高畫質海報從高畫質原圖自動裁切直式 poster橫式 fanart使用作品的完整橫式封套儲存全部劇照將 Sample Images 儲存到 extrafanart 資料夾儲存方式直接儲存並覆寫(略過預覽)略過儲存前變更預覽;已有 NFO 或圖片將直接覆寫。影片檔案永遠不會被覆寫。將影片重新命名為番號只變更影片檔名,不變更副檔名記住儲存偏好記住本列儲存方式、目標位置和輸出選項,包括直接儲存並覆寫
- 目標位置影片所在位置(不移動)來源位置的番號資料夾自訂根目錄的番號資料夾自訂根目錄可直接貼上本機絕對路徑或 UNC 網路共用路徑最近目錄最近目錄 ({0}) ▾選擇或管理最近使用的自訂根目錄選擇影片後顯示最終路徑請選擇自訂目標根目錄;選擇影片後將顯示最終路徑最終影片:{0}傳輸方式:安全複製 + SHA-256 校驗,成功後移除來源取消目前操作準備就緒,請選擇或拖入一個影片介面語言已切換v1.0.0 · 穩定版
+ 目標位置影片所在位置(不移動)來源位置的番號資料夾自訂根目錄的番號資料夾自訂根目錄可直接貼上本機絕對路徑或 UNC 網路共用路徑最近目錄最近目錄 ({0}) ▾選擇或管理最近使用的自訂根目錄選擇影片後顯示最終路徑請選擇自訂目標根目錄;選擇影片後將顯示最終路徑最終影片:{0}傳輸方式:安全複製 + SHA-256 校驗,成功後移除來源取消目前操作準備就緒,請選擇或拖入一個影片介面語言已切換v1.1.1 · 穩定版
JAVLibrary 瀏覽器完成驗證並開啟正確的影片詳情頁讀取目前影片軟體只會匯入目前頁面的影片資料。內建瀏覽器可能保留網站驗證所需的 Cookie。無法啟動內建瀏覽器:{0}
請確認已安裝 Microsoft Edge WebView2 Runtime。目前頁面不是影片詳情頁。請先在網頁中開啟正確影片,再點選讀取。
儲存前變更預覽只有點選「確認並執行」後才會寫入 metadata 或移動影片。目前影片最終影片我確認覆寫上面列出的已有 metadata 檔案影片目標衝突時軟體不會執行儲存。請取消並處理目標影片衝突後重新預覽。偵測到 {0} 個已有 metadata 檔案。已啟用覆寫;確認後會取代這些檔案。偵測到 {0} 個已有 metadata 檔案。必須明確確認後才能覆寫。影片檔案永遠不會被覆寫。
建立資料夾移動影片重新命名影片移動並重新命名複製並校驗更新保持不變取代圖片覆寫產生衝突
選擇一個影片選擇媒體庫根目錄選擇一張完整封套圖片檔案|*.jpg;*.jpeg;*.png;*.webp|所有檔案|*.*移除目前記錄清除最近記錄選擇本機完整封套…選擇一張圖片,統一產生 poster 與 fanart支援 JPG、JPEG、PNG 與 WEBP
- 已還原儲存偏好:已啟用直接儲存並覆寫已還原上次明確記住的儲存偏好目前為手動模式,可直接填寫資料並儲存正在同時取得 LibreDMM 與 R18.dev…正在取得影片資料…;{0} 未傳回結果,找到 {0} 張劇照,沒有獨立劇照已從 {0} 讀取 {1}已從 {0} 讀取新資料;可逐欄位切回本機 NFO;fanart 預覽未載入;封面預覽未載入,不影響資料編輯網站要求瀏覽器驗證,正在開啟內建瀏覽器…已取消,影片和 metadata 均未修改正在安全產生並提交檔案…extrafanart({0} 張);fanart 來自完整封套;影片已整理為 {0}sidecar 已安全遷移沒有需要寫入的變更儲存完成:{0}{1}{2}正在檢查影片旁的本機 metadata…
+ 已還原儲存偏好:已啟用直接儲存並覆寫已還原上次明確記住的儲存偏好目前為手動模式,可直接填寫資料並儲存正在同時取得 LibreDMM 與 R18.dev…正在取得影片資料…;{0} 未傳回結果,找到 {0} 張劇照,沒有獨立劇照已從 {0} 讀取 {1}已從 {0} 讀取新資料;可逐欄位切回本機 NFO;fanart 預覽未載入;封面預覽未載入,不影響資料編輯網站要求瀏覽器驗證,正在開啟內建瀏覽器…已取消,影片和 metadata 均未修改正在安全產生並提交檔案…extrafanart({0} 張);fanart 來自完整封套;影片已整理為 {0}sidecar 已安全遷移沒有需要寫入的變更未找到劇照,已自動略過儲存完成:{0}{1}{2}正在檢查影片旁的本機 metadata…
已移除目前目錄的歷史記錄;目前路徑保持不變已清除最近目標根目錄;目前路徑保持不變自訂目標根目錄目前無法使用:{0}。請重新連線或選擇其他目錄;程式不會自動建立該根目錄。本機 NFO 無法安全讀取;修復或移走後重新選擇影片儲存時只更新受管理欄位,並保留偵測到的未知 XML儲存時只更新受管理欄位無法檢查影片旁的本機檔案:{0}已識別番號 {0},未找到同名本機 NFO,可以搜尋資料未找到同名本機 NFO,也未從檔名識別番號,請手動輸入已從本機 NFO 載入(可安全更新):{0}{1}偵測到本機 NFO,但無法安全讀取,原檔未修改:{0};{1}
已忽略 {0} 個無效本機圖片(請見日誌);已忽略 {0} 個無效圖片;預覽載入不完整本機圖片已載入({0}){1}{2}僅 poster,缺少 fanart僅 fanart,缺少 poster無可用圖片正在讀取瀏覽器中的資料…;可逐欄位切回本機 NFO已讀取瀏覽器中的新資料 {0}{1}已將「{0}」切換為 {1}正在載入 {0} 封套…封套與 fanart 已同時切換為 {0}{0} 封套預覽載入不完整,可改選其他來源正在讀取本機完整封套…已選擇本機完整封套:{0};poster 與 fanart 將由同一來源產生操作已取消;未完成的檔案交易已安全復原正在取消並復原檔案,請稍候…日誌目錄:{0}
請先輸入影片番號。請先選擇一個影片檔案。偵測到無法安全讀取的本機 NFO。為保護原檔,必須修復或移走該 NFO 後重新選擇影片,目前不能儲存。請選擇支援的影片檔案。無法開啟日誌目錄:{0}啟動參數未提供可讀取的影片路徑。無法讀取啟動參數中的影片路徑。
@@ -23,4 +23,17 @@
影片檔案|*.mp4;*.m4v;*.mkv;*.avi;*.wmv;*.mov;*.webm;*.ts;*.m2ts|所有檔案|*.*
建立番號資料夾移動影片重新命名影片移動並重新命名影片安全複製並校驗影片,成功後移除來源產生 NFO覆寫 NFO更新 NFO更新 NFO(保留未知 XML)NFO 內容保持不變產生 {0}取代 {0}{0} 內容保持不變本機 {0} 缺少,保持缺少遷移並保持 {0}產生劇照取代劇照自訂目標根目錄路徑已被檔案占用:{0}自訂目標根目錄目前無法使用,程式不會自動建立該根目錄:{0}目標資料夾路徑已被檔案占用:{0}目標影片已存在,軟體不會覆寫影片:{0}正在準備 metadata 與安全交易…正在將影片複製到目標暫存區…正在將影片複製到目標暫存區… {0}%正在校驗目標影片 SHA-256…正在校驗目標影片 SHA-256… {0}%正在提交目標影片與 metadata…目標校驗與提交完成,正在移除來源影片…安全儲存已完成
資料來源 {0} 在 {1:0} 秒內沒有回應,請稍後重試或更換來源。
+ 略過跨磁碟 SHA-256 校驗(較快,風險自負)
+ 只檢查複製完成與檔案大小,不逐位元組校驗影片內容
+ 傳輸方式:快速跨磁碟複製,只檢查檔案大小(風險自負)
+ 快速複製
+ 快速複製影片;只檢查檔案大小,不校驗內容,成功後移除來源
+ 記住儲存方式、目標位置、跨磁碟校驗和輸出選項,包括直接儲存並覆寫
+ 目標複製與提交完成,正在移除來源影片…
+ v1.1.1 · 穩定版
+ 拖入一個影片檔案或番號資料夾
+ 準備就緒,請選擇影片,或拖入一個影片檔案或番號資料夾
+ 此資料夾頂層找不到支援的影片檔案。
+ 此資料夾包含多個支援的影片檔案,請改為拖入單一影片檔案。
+ 無法讀取拖入的資料夾:{0}
diff --git a/JavMetaLite.App/SavePreviewWindow.xaml.cs b/JavMetaLite.App/SavePreviewWindow.xaml.cs
index 294fd5c..eb07497 100644
--- a/JavMetaLite.App/SavePreviewWindow.xaml.cs
+++ b/JavMetaLite.App/SavePreviewWindow.xaml.cs
@@ -98,6 +98,7 @@ private static PreviewRow CreateRow(PlannedFileChange change)
PlannedChangeKind.RenameVideo => (LocalizationService.Get("Preview.Action.RenameVideo"), "#193152", "#8DB8FF"),
PlannedChangeKind.MoveAndRenameVideo => (LocalizationService.Get("Preview.Action.MoveAndRename"), "#193152", "#8DB8FF"),
PlannedChangeKind.CopyAndVerifyVideo => (LocalizationService.Get("Preview.Action.CopyVerify"), "#193152", "#8DB8FF"),
+ PlannedChangeKind.CopyVideo => (LocalizationService.Get("Preview.Action.CopyFast"), "#4A3218", "#FFD18A"),
PlannedChangeKind.UpdateFile => (LocalizationService.Get("Preview.Action.Update"), "#1B3C36", "#72E3C1"),
PlannedChangeKind.KeepFile => (LocalizationService.Get("Preview.Action.Keep"), "#252D38", "#A9B7C8"),
PlannedChangeKind.ReplaceImage => (LocalizationService.Get("Preview.Action.ReplaceImage"), "#4A3218", "#FFD18A"),
@@ -130,6 +131,7 @@ private static string LocalizeDescription(PlannedFileChange change)
PlannedChangeKind.RenameVideo => "Preview.Description.RenameVideo",
PlannedChangeKind.MoveAndRenameVideo => "Preview.Description.MoveAndRename",
PlannedChangeKind.CopyAndVerifyVideo => "Preview.Description.CopyVerify",
+ PlannedChangeKind.CopyVideo => "Preview.Description.CopyFast",
_ => null
};
if (videoKey is not null)
diff --git a/JavMetaLite.Core/Models/AppPreferences.cs b/JavMetaLite.Core/Models/AppPreferences.cs
index 9f9127b..7e4b1fb 100644
--- a/JavMetaLite.Core/Models/AppPreferences.cs
+++ b/JavMetaLite.Core/Models/AppPreferences.cs
@@ -2,7 +2,7 @@ namespace JavMetaLite.Core.Models;
public sealed record AppPreferences
{
- public const int CurrentSchemaVersion = 4;
+ public const int CurrentSchemaVersion = 5;
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
@@ -12,6 +12,9 @@ public sealed record AppPreferences
public bool DirectSaveOverwrite { get; init; }
+ public CrossVolumeVerificationMode CrossVolumeVerification { get; init; } =
+ CrossVolumeVerificationMode.FullSha256;
+
public OrganizationTargetMode TargetMode { get; init; } = OrganizationTargetMode.VideoDirectory;
public string? CustomRootDirectory { get; init; }
diff --git a/JavMetaLite.Core/Models/OrganizationOptions.cs b/JavMetaLite.Core/Models/OrganizationOptions.cs
index 1fc331a..3349b7d 100644
--- a/JavMetaLite.Core/Models/OrganizationOptions.cs
+++ b/JavMetaLite.Core/Models/OrganizationOptions.cs
@@ -7,6 +7,12 @@ public enum OrganizationTargetMode
CustomRootNumberFolder
}
+public enum CrossVolumeVerificationMode
+{
+ FullSha256,
+ FileSizeOnly
+}
+
public sealed record OrganizationOptions
{
public OrganizationOptions(bool createMovieFolder, bool renameVideo)
@@ -14,18 +20,23 @@ public OrganizationOptions(bool createMovieFolder, bool renameVideo)
createMovieFolder
? OrganizationTargetMode.SourceNumberFolder
: OrganizationTargetMode.VideoDirectory,
- renameVideo)
+ renameVideo,
+ crossVolumeVerification: CrossVolumeVerificationMode.FullSha256)
{
}
public OrganizationOptions(
OrganizationTargetMode targetMode,
bool renameVideo,
- string? customRootDirectory = null)
+ string? customRootDirectory = null,
+ CrossVolumeVerificationMode crossVolumeVerification = CrossVolumeVerificationMode.FullSha256)
{
TargetMode = targetMode;
RenameVideo = renameVideo;
CustomRootDirectory = customRootDirectory;
+ CrossVolumeVerification = Enum.IsDefined(crossVolumeVerification)
+ ? crossVolumeVerification
+ : CrossVolumeVerificationMode.FullSha256;
}
public OrganizationTargetMode TargetMode { get; }
@@ -34,6 +45,8 @@ public OrganizationOptions(
public string? CustomRootDirectory { get; }
+ public CrossVolumeVerificationMode CrossVolumeVerification { get; }
+
public bool CreateMovieFolder => TargetMode is not OrganizationTargetMode.VideoDirectory;
public bool UsesCustomRoot => TargetMode is OrganizationTargetMode.CustomRootNumberFolder;
@@ -63,6 +76,7 @@ public enum PlannedChangeKind
RenameVideo,
MoveAndRenameVideo,
CopyAndVerifyVideo,
+ CopyVideo,
CreateFile,
OverwriteFile,
UpdateFile,
@@ -147,6 +161,7 @@ public enum FileTransactionStage
VerifyingMovie,
Committing,
RetiringSource,
+ RetiringSourceFast,
Completed
}
diff --git a/JavMetaLite.Core/Services/AppPreferencesStore.cs b/JavMetaLite.Core/Services/AppPreferencesStore.cs
index 18b227e..2b88cd5 100644
--- a/JavMetaLite.Core/Services/AppPreferencesStore.cs
+++ b/JavMetaLite.Core/Services/AppPreferencesStore.cs
@@ -64,6 +64,13 @@ public AppPreferencesLoadResult Load()
{
preferences = preferences with { UiLanguage = UiLanguageCodes.SimplifiedChinese };
}
+ if (preferences.SchemaVersion <= 4)
+ {
+ preferences = preferences with
+ {
+ CrossVolumeVerification = CrossVolumeVerificationMode.FullSha256
+ };
+ }
if (!preferences.RememberSavePreferences)
{
@@ -171,6 +178,9 @@ private static AppPreferences Normalize(
TargetMode = Enum.IsDefined(preferences.TargetMode)
? preferences.TargetMode
: OrganizationTargetMode.VideoDirectory,
+ CrossVolumeVerification = Enum.IsDefined(preferences.CrossVolumeVerification)
+ ? preferences.CrossVolumeVerification
+ : CrossVolumeVerificationMode.FullSha256,
CustomRootDirectory = customRoot,
RecentCustomRootDirectories = recentRoots.ToArray()
};
diff --git a/JavMetaLite.Core/Services/FileOrganizationService.cs b/JavMetaLite.Core/Services/FileOrganizationService.cs
index 80ea937..8bf63ea 100644
--- a/JavMetaLite.Core/Services/FileOrganizationService.cs
+++ b/JavMetaLite.Core/Services/FileOrganizationService.cs
@@ -69,7 +69,9 @@ public static SavePlan BuildPlan(
var nameChanges = !Path.GetFileName(sourceVideoPath)
.Equals(Path.GetFileName(targetVideoPath), StringComparison.OrdinalIgnoreCase);
var kind = pathPlan.RequiresVerifiedCopy
- ? PlannedChangeKind.CopyAndVerifyVideo
+ ? organizationOptions.CrossVolumeVerification is CrossVolumeVerificationMode.FullSha256
+ ? PlannedChangeKind.CopyAndVerifyVideo
+ : PlannedChangeKind.CopyVideo
: directoryChanges && nameChanges
? PlannedChangeKind.MoveAndRenameVideo
: directoryChanges
@@ -78,6 +80,7 @@ public static SavePlan BuildPlan(
var description = kind switch
{
PlannedChangeKind.CopyAndVerifyVideo => "安全复制并校验影片,成功后移除来源",
+ PlannedChangeKind.CopyVideo => "快速复制影片并检查文件大小,成功后移除来源",
PlannedChangeKind.MoveAndRenameVideo => "移动并重命名影片",
PlannedChangeKind.MoveVideo => "移动影片",
_ => "重命名影片"
@@ -267,11 +270,14 @@ public async Task ExecuteAsync(
var stagingVideoPath = Path.Combine(
sourceStagingRoot,
plan.TargetBaseName + Path.GetExtension(plan.TargetVideoPath));
- var verifiedCopy = plan.RequiresVerifiedVideoCopy && plan.VideoWillMove;
- var targetStagingRoot = verifiedCopy
+ var crossVolumeCopy = plan.RequiresVerifiedVideoCopy && plan.VideoWillMove;
+ var fullVerification = crossVolumeCopy &&
+ plan.OrganizationOptions.CrossVolumeVerification is
+ CrossVolumeVerificationMode.FullSha256;
+ var targetStagingRoot = crossVolumeCopy
? Path.Combine(plan.TargetDirectory, $".JavMetaLite-target-{operationId}.tmp")
: sourceStagingRoot;
- var targetPayloadRoot = verifiedCopy
+ var targetPayloadRoot = crossVolumeCopy
? Path.Combine(targetStagingRoot, "payload")
: sourceStagingRoot;
var backupRoot = Path.Combine(targetStagingRoot, "backup");
@@ -286,7 +292,7 @@ public async Task ExecuteAsync(
var operationSucceeded = false;
var rollbackSucceeded = false;
- if (verifiedCopy)
+ if (crossVolumeCopy)
{
EnsureTargetCapacity(plan.SourceVideoPath, plan.TargetDirectory);
}
@@ -295,7 +301,7 @@ public async Task ExecuteAsync(
$"开始执行保存计划 source={plan.SourceVideoPath} target={plan.TargetVideoPath} " +
$"organize={plan.OrganizationOptions.CreateMovieFolder} rename={plan.OrganizationOptions.RenameVideo} " +
$"roundTrip={plan.NfoWriteContext?.LocalBundle is not null} transfers={plan.SidecarTransfers.Count} " +
- $"verifiedCopy={verifiedCopy}");
+ $"crossVolumeCopy={crossVolumeCopy} verification={plan.OrganizationOptions.CrossVolumeVerification}");
try
{
@@ -342,7 +348,7 @@ public async Task ExecuteAsync(
string? targetStagedVideoPath = null;
IReadOnlyList commitStagedPaths = stagedPaths;
- if (verifiedCopy)
+ if (crossVolumeCopy)
{
Directory.CreateDirectory(targetPayloadRoot);
var targetCopies = new List(stagedPaths.Count);
@@ -361,26 +367,42 @@ public async Task ExecuteAsync(
targetStagingRoot,
"movie",
Path.GetFileName(plan.TargetVideoPath));
- var sourceHash = await CopyMovieWithHashAsync(
+ var sourceLength = new FileInfo(plan.SourceVideoPath).Length;
+ var sourceHash = await CopyMovieAsync(
plan.SourceVideoPath,
targetStagedVideoPath,
+ fullVerification,
progress,
cancellationToken);
- progress?.Report(new FileTransactionProgress(
- FileTransactionStage.VerifyingMovie,
- "正在校验目标影片 SHA-256…",
- 0,
- new FileInfo(targetStagedVideoPath).Length,
- targetStagedVideoPath));
- var targetHash = await ComputeSha256Async(
- targetStagedVideoPath,
- progress,
- cancellationToken);
- if (!sourceHash.Equals(targetHash, StringComparison.OrdinalIgnoreCase))
+ var targetLength = new FileInfo(targetStagedVideoPath).Length;
+ if (sourceLength != targetLength)
+ {
+ throw new IOException(
+ $"目标影片大小检查失败;来源影片已保留,未提交目标文件。" +
+ $"来源 {sourceLength} 字节,目标 {targetLength} 字节。");
+ }
+ if (fullVerification)
{
- throw new IOException("目标影片 SHA-256 校验失败;来源影片已保留,未提交目标文件。");
+ progress?.Report(new FileTransactionProgress(
+ FileTransactionStage.VerifyingMovie,
+ "正在校验目标影片 SHA-256…",
+ 0,
+ targetLength,
+ targetStagedVideoPath));
+ var targetHash = await ComputeSha256Async(
+ targetStagedVideoPath,
+ progress,
+ cancellationToken);
+ if (!string.Equals(sourceHash, targetHash, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new IOException("目标影片 SHA-256 校验失败;来源影片已保留,未提交目标文件。");
+ }
+ AppLog.Info($"跨卷影片复制校验完成 sha256={sourceHash} bytes={targetLength}");
+ }
+ else
+ {
+ AppLog.Warning($"跨卷影片使用快速传输,仅检查文件大小 bytes={targetLength}");
}
- AppLog.Info($"跨卷影片复制校验完成 sha256={sourceHash} bytes={new FileInfo(targetStagedVideoPath).Length}");
}
var mappings = commitStagedPaths
@@ -422,7 +444,7 @@ public async Task ExecuteAsync(
committedOutputs.Add(mapping.FinalPath);
}
- if (verifiedCopy)
+ if (crossVolumeCopy)
{
cancellationToken.ThrowIfCancellationRequested();
File.Move(targetStagedVideoPath!, plan.TargetVideoPath);
@@ -445,11 +467,15 @@ public async Task ExecuteAsync(
if (plan.VideoWillMove)
{
- if (verifiedCopy)
+ if (crossVolumeCopy)
{
progress?.Report(new FileTransactionProgress(
- FileTransactionStage.RetiringSource,
- "目标校验与提交完成,正在移除来源影片…"));
+ fullVerification
+ ? FileTransactionStage.RetiringSource
+ : FileTransactionStage.RetiringSourceFast,
+ fullVerification
+ ? "目标校验与提交完成,正在移除来源影片…"
+ : "目标复制与提交完成,正在移除来源影片…"));
sourceVideoBackupPath = Path.Combine(
sourceRetireRoot,
"movie",
@@ -476,7 +502,11 @@ public async Task ExecuteAsync(
AppLog.Info($"保存计划完成 video={plan.TargetVideoPath} outputs={committedOutputs.Count} moved={videoMoved}");
progress?.Report(new FileTransactionProgress(
FileTransactionStage.Completed,
- verifiedCopy ? "安全复制、校验与提交已完成" : "安全保存已完成"));
+ crossVolumeCopy
+ ? fullVerification
+ ? "安全复制、校验与提交已完成"
+ : "快速跨卷复制与提交已完成"
+ : "安全保存已完成"));
operationSucceeded = true;
rollbackSucceeded = true;
return new OrganizedSaveResult(finalResult, plan.TargetVideoPath, videoMoved);
@@ -487,7 +517,7 @@ public async Task ExecuteAsync(
var rollbackErrors = Rollback(
plan,
videoMoved,
- verifiedCopy,
+ crossVolumeCopy,
targetVideoCommitted,
sourceVideoRetired,
sourceVideoBackupPath,
@@ -496,7 +526,7 @@ public async Task ExecuteAsync(
rollbackSucceeded = rollbackErrors.Count == 0;
if (!rollbackSucceeded)
{
- var recoveryPaths = verifiedCopy
+ var recoveryPaths = crossVolumeCopy
? $"{sourceStagingRoot};{targetStagingRoot}"
: sourceStagingRoot;
AppLog.Error($"文件恢复不完整,临时备份保留在 {recoveryPaths}", new AggregateException(rollbackErrors));
@@ -712,9 +742,10 @@ private static void EnsureTargetCapacity(string sourceVideoPath, string targetDi
}
}
- private static async Task CopyMovieWithHashAsync(
+ private static async Task CopyMovieAsync(
string sourcePath,
string destinationPath,
+ bool computeSha256,
IProgress? progress,
CancellationToken cancellationToken)
{
@@ -734,7 +765,9 @@ private static async Task CopyMovieWithHashAsync(
FileShare.None,
bufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan);
- using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
+ using var hash = computeSha256
+ ? IncrementalHash.CreateHash(HashAlgorithmName.SHA256)
+ : null;
var buffer = GC.AllocateUninitializedArray(bufferSize);
var totalBytes = source.Length;
long copiedBytes = 0;
@@ -750,7 +783,7 @@ private static async Task CopyMovieWithHashAsync(
break;
}
- hash.AppendData(buffer, 0, read);
+ hash?.AppendData(buffer, 0, read);
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
copiedBytes += read;
var percentage = totalBytes <= 0 ? 100 : (int)(copiedBytes * 100L / totalBytes);
@@ -767,7 +800,7 @@ private static async Task CopyMovieWithHashAsync(
}
await destination.FlushAsync(cancellationToken);
- return Convert.ToHexString(hash.GetHashAndReset());
+ return hash is null ? null : Convert.ToHexString(hash.GetHashAndReset());
void ReportProgress(
FileTransactionStage stage,
diff --git a/JavMetaLite.Core/Services/OutputService.cs b/JavMetaLite.Core/Services/OutputService.cs
index d45bede..7236f65 100644
--- a/JavMetaLite.Core/Services/OutputService.cs
+++ b/JavMetaLite.Core/Services/OutputService.cs
@@ -83,10 +83,9 @@ public async Task SaveAsync(
var extraImages = options.DownloadExtrafanart
? screenshots.ToArray()
: [];
- if (options.DownloadExtrafanart && extraImages.Length == 0 &&
- !options.WriteNfo && !options.DownloadPoster && !options.DownloadFanart)
+ if (options.DownloadExtrafanart && extraImages.Length == 0)
{
- throw new InvalidOperationException("没有找到可保存的 Sample Images。 ");
+ AppLog.Info($"未找到可保存的 Sample Images,已跳过 extrafanart id={metadata.Id}");
}
var extraDirectory = Path.Combine(directory, "extrafanart");
diff --git a/JavMetaLite.Core/Services/VideoFileSupport.cs b/JavMetaLite.Core/Services/VideoFileSupport.cs
index 18d2625..c92e0bd 100644
--- a/JavMetaLite.Core/Services/VideoFileSupport.cs
+++ b/JavMetaLite.Core/Services/VideoFileSupport.cs
@@ -1,5 +1,20 @@
namespace JavMetaLite.Core.Services;
+public enum VideoInputPathStatus
+{
+ Success,
+ UnsupportedPath,
+ FolderHasNoVideo,
+ FolderHasMultipleVideos
+}
+
+public sealed record VideoInputPathResolution(
+ VideoInputPathStatus Status,
+ string? VideoPath = null)
+{
+ public bool Success => Status is VideoInputPathStatus.Success && VideoPath is not null;
+}
+
public static class VideoFileSupport
{
private static readonly HashSet Extensions = new(StringComparer.OrdinalIgnoreCase)
@@ -18,4 +33,39 @@ public static bool HasSupportedExtension(string? path) =>
public static bool IsSupportedExistingFile(string? path) =>
!string.IsNullOrWhiteSpace(path) && File.Exists(path) && HasSupportedExtension(path);
+
+ public static VideoInputPathResolution ResolveInputPath(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return new VideoInputPathResolution(VideoInputPathStatus.UnsupportedPath);
+ }
+
+ if (File.Exists(path))
+ {
+ return HasSupportedExtension(path)
+ ? new VideoInputPathResolution(
+ VideoInputPathStatus.Success,
+ Path.GetFullPath(path))
+ : new VideoInputPathResolution(VideoInputPathStatus.UnsupportedPath);
+ }
+
+ if (!Directory.Exists(path))
+ {
+ return new VideoInputPathResolution(VideoInputPathStatus.UnsupportedPath);
+ }
+
+ var videos = Directory
+ .EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly)
+ .Where(HasSupportedExtension)
+ .Take(2)
+ .Select(Path.GetFullPath)
+ .ToArray();
+ return videos.Length switch
+ {
+ 0 => new VideoInputPathResolution(VideoInputPathStatus.FolderHasNoVideo),
+ 1 => new VideoInputPathResolution(VideoInputPathStatus.Success, videos[0]),
+ _ => new VideoInputPathResolution(VideoInputPathStatus.FolderHasMultipleVideos)
+ };
+ }
}
diff --git a/JavMetaLite.RegressionTests/FileOrganizationRegressionTests.cs b/JavMetaLite.RegressionTests/FileOrganizationRegressionTests.cs
index 98bec4e..d6ae97a 100644
--- a/JavMetaLite.RegressionTests/FileOrganizationRegressionTests.cs
+++ b/JavMetaLite.RegressionTests/FileOrganizationRegressionTests.cs
@@ -21,6 +21,7 @@ internal static class FileOrganizationRegressionTests
new("target", "同卷自定义根目录安全执行并保持影片字节", TestSameVolumeCustomTargetExecution),
new("target", "自定义根目录校验、冲突、跨盘符与 UNC 规划", TestCustomTargetValidation),
new("transfer", "安全复制、SHA-256 校验并在提交后移除来源", TestVerifiedCopySuccess),
+ new("transfer", "快速跨盘复制仅检查大小并跳过内容校验", TestFastCrossVolumeCopySuccess),
new("transfer", "复制期间取消会保留来源并清理目标", TestVerifiedCopyCancellation),
new("transfer", "SHA-256 不一致会拒绝提交并保留来源", TestVerifiedCopyHashMismatch),
new("transfer", "复制后出现目标影片冲突会保留两边文件", TestVerifiedCopyLateTargetConflict),
@@ -303,6 +304,22 @@ private static Task TestCustomTargetValidation()
crossDrive.RequiresVerifiedCopy,
"A different-drive target did not select the verified-copy transaction.");
+ var fastCrossDrivePlan = FileOrganizationService.BuildPlan(
+ sourcePath,
+ Metadata("IPX-888", "快速跨盘预览"),
+ NfoOnly(),
+ new OrganizationOptions(
+ OrganizationTargetMode.CustomRootNumberFolder,
+ true,
+ @"Z:\Jellyfin\Movies",
+ CrossVolumeVerificationMode.FileSizeOnly));
+ AssertEx.True(
+ fastCrossDrivePlan.Changes.Any(change => change.Kind is PlannedChangeKind.CopyVideo),
+ "Fast mode was not shown as a fast copy in the save plan.");
+ AssertEx.False(
+ fastCrossDrivePlan.Changes.Any(change => change.Kind is PlannedChangeKind.CopyAndVerifyVideo),
+ "Fast mode was incorrectly shown as full SHA-256 verification.");
+
var unc = OrganizationPathPlanner.Resolve(
sourcePath,
"IPX-888",
@@ -359,6 +376,53 @@ private static async Task TestVerifiedCopySuccess()
workspace.AssertNoTemporaryArtifacts();
}
+ private static async Task TestFastCrossVolumeCopySuccess()
+ {
+ using var workspace = new TestWorkspace("fast-cross-volume-copy-success");
+ var sourceBytes = Enumerable.Range(0, 3 * 1024 * 1024 + 911)
+ .Select(index => (byte)(index % 239))
+ .ToArray();
+ var sourcePath = workspace.WriteFile("incoming/fast-source.mkv", sourceBytes);
+ var sourceHash = AssertEx.Sha256(sourcePath);
+ var customRoot = workspace.CreateDirectory("library");
+ var metadata = Metadata("IPX-895", "快速跨盘复制");
+ var plan = FileOrganizationService.BuildPlan(
+ sourcePath,
+ metadata,
+ NfoOnly(),
+ new OrganizationOptions(
+ OrganizationTargetMode.CustomRootNumberFolder,
+ true,
+ customRoot,
+ CrossVolumeVerificationMode.FileSizeOnly)) with
+ {
+ RequiresVerifiedVideoCopy = true
+ };
+ var stages = new List();
+ var progress = new InlineProgress(update => stages.Add(update.Stage));
+ using var outputService = new OutputService();
+ var result = await new FileOrganizationService(outputService)
+ .ExecuteAsync(plan, metadata, false, CancellationToken.None, progress);
+
+ AssertEx.FileDoesNotExist(sourcePath);
+ AssertEx.FileExists(result.VideoPath);
+ AssertEx.Equal(sourceBytes.LongLength, new FileInfo(result.VideoPath).Length);
+ AssertEx.Equal(sourceHash, AssertEx.Sha256(result.VideoPath));
+ AssertEx.True(
+ stages.Contains(FileTransactionStage.CopyingMovie),
+ "Fast mode did not report copy progress.");
+ AssertEx.False(
+ stages.Contains(FileTransactionStage.VerifyingMovie),
+ "Fast mode unexpectedly reread the complete target for SHA-256 verification.");
+ AssertEx.True(
+ stages.Contains(FileTransactionStage.RetiringSourceFast),
+ "Fast mode did not report source retirement.");
+ AssertEx.True(
+ stages.Contains(FileTransactionStage.Completed),
+ "Fast mode did not report completion.");
+ workspace.AssertNoTemporaryArtifacts();
+ }
+
private static async Task TestVerifiedCopyCancellation()
{
using var workspace = new TestWorkspace("verified-copy-cancel");
diff --git a/JavMetaLite.SmokeTests/Program.cs b/JavMetaLite.SmokeTests/Program.cs
index 9d1dacc..0cc66ee 100644
--- a/JavMetaLite.SmokeTests/Program.cs
+++ b/JavMetaLite.SmokeTests/Program.cs
@@ -93,7 +93,8 @@
{
("番号识别", TestMovieIdParser),
("v0.8 启动影片参数解析", TestStartupVideoRequestResolver),
- ("v0.9 四语言与保存偏好原子存储", TestAppPreferencesStore),
+ ("v1.1 影片文件与番号文件夹输入解析", TestVideoInputPathResolver),
+ ("v1.1 四语言、跨盘校验与保存偏好原子存储", TestAppPreferencesStore),
("LibreDMM JSON 解析与清理", TestLibreDmmParser),
("多来源字段补全", TestMetadataMerge),
("v0.5 多来源搜索编排", TestMetadataSearchCoordinator),
@@ -106,6 +107,7 @@
("高清海报自动裁切", TestPosterCropping),
("NFO 生成", TestNfoWriter),
("完整封套 fanart 与 Sample Images 输出", TestArtworkOutput),
+ ("v1.1 无 Sample Images 时自动跳过", TestMissingSampleImagesAreSkipped),
("v0.6 本地完整封套输出", TestLocalCompleteCoverOutput),
("v0.4 文件整理计划与安全执行", TestFileOrganization),
("v0.6 本地 sidecar 定位", TestLocalSidecarLocator),
@@ -200,6 +202,50 @@ static Task TestStartupVideoRequestResolver()
return Task.CompletedTask;
}
+static Task TestVideoInputPathResolver()
+{
+ var root = Path.Combine(Path.GetTempPath(), $"JavMetaLite.VideoInput.{Guid.NewGuid():N}");
+ Directory.CreateDirectory(root);
+ try
+ {
+ var directVideo = Path.Combine(root, "IPX-123.mp4");
+ File.WriteAllBytes(directVideo, [0x01]);
+ var direct = VideoFileSupport.ResolveInputPath(directVideo);
+ AssertEqual(VideoInputPathStatus.Success.ToString(), direct.Status.ToString());
+ AssertEqual(Path.GetFullPath(directVideo), direct.VideoPath);
+
+ var numberFolder = Path.Combine(root, "SONE-855");
+ Directory.CreateDirectory(numberFolder);
+ var folderVideo = Path.Combine(numberFolder, "SONE-855.mkv");
+ File.WriteAllBytes(folderVideo, [0x02]);
+ File.WriteAllText(Path.Combine(numberFolder, "SONE-855.nfo"), "");
+ var folder = VideoFileSupport.ResolveInputPath(numberFolder);
+ AssertEqual(VideoInputPathStatus.Success.ToString(), folder.Status.ToString());
+ AssertEqual(Path.GetFullPath(folderVideo), folder.VideoPath);
+
+ var emptyFolder = Path.Combine(root, "FNS-121");
+ Directory.CreateDirectory(emptyFolder);
+ Directory.CreateDirectory(Path.Combine(emptyFolder, "nested"));
+ File.WriteAllBytes(Path.Combine(emptyFolder, "nested", "FNS-121.mp4"), [0x03]);
+ AssertEqual(
+ VideoInputPathStatus.FolderHasNoVideo.ToString(),
+ VideoFileSupport.ResolveInputPath(emptyFolder).Status.ToString());
+
+ File.WriteAllBytes(Path.Combine(numberFolder, "extra.avi"), [0x04]);
+ AssertEqual(
+ VideoInputPathStatus.FolderHasMultipleVideos.ToString(),
+ VideoFileSupport.ResolveInputPath(numberFolder).Status.ToString());
+ AssertEqual(
+ VideoInputPathStatus.UnsupportedPath.ToString(),
+ VideoFileSupport.ResolveInputPath(Path.Combine(root, "missing")).Status.ToString());
+ return Task.CompletedTask;
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+}
+
static Task TestAppPreferencesStore()
{
var root = Path.Combine(Path.GetTempPath(), $"JavMetaLite.PreferencesTests.{Guid.NewGuid():N}");
@@ -211,6 +257,7 @@ static Task TestAppPreferencesStore()
AssertEqual(UiLanguageCodes.System, missing.Preferences.UiLanguage);
AssertEqual("False", missing.Preferences.RememberSavePreferences.ToString());
AssertEqual("False", missing.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), missing.Preferences.CrossVolumeVerification.ToString());
AssertEqual(OrganizationTargetMode.VideoDirectory.ToString(), missing.Preferences.TargetMode.ToString());
AssertEqual("True", missing.Preferences.WriteNfo.ToString());
AssertEqual("True", missing.Preferences.DownloadPoster.ToString());
@@ -224,6 +271,7 @@ static Task TestAppPreferencesStore()
UiLanguage = UiLanguageCodes.Japanese,
RememberSavePreferences = true,
DirectSaveOverwrite = true,
+ CrossVolumeVerification = CrossVolumeVerificationMode.FileSizeOnly,
TargetMode = OrganizationTargetMode.CustomRootNumberFolder,
CustomRootDirectory = $" {customRoot} ",
RecentCustomRootDirectories =
@@ -245,16 +293,18 @@ static Task TestAppPreferencesStore()
AssertEqual("True", File.Exists(store.SettingsPath).ToString());
var json = File.ReadAllText(store.SettingsPath);
- AssertEqual("True", json.Contains("\"SchemaVersion\": 4", StringComparison.Ordinal).ToString());
+ AssertEqual("True", json.Contains("\"SchemaVersion\": 5", StringComparison.Ordinal).ToString());
AssertEqual("True", json.Contains("\"UiLanguage\": \"ja\"", StringComparison.Ordinal).ToString());
AssertEqual("True", json.Contains("\"CustomRootNumberFolder\"", StringComparison.Ordinal).ToString());
AssertEqual("True", json.Contains("\"DirectSaveOverwrite\": true", StringComparison.Ordinal).ToString());
+ AssertEqual("True", json.Contains("\"CrossVolumeVerification\": \"FileSizeOnly\"", StringComparison.Ordinal).ToString());
AssertEqual("0", Directory.EnumerateFiles(root, "*.tmp").Count().ToString());
var loaded = store.Load();
AssertEqual("True", loaded.Preferences.RememberSavePreferences.ToString());
AssertEqual(UiLanguageCodes.Japanese, loaded.Preferences.UiLanguage);
AssertEqual("True", loaded.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FileSizeOnly.ToString(), loaded.Preferences.CrossVolumeVerification.ToString());
AssertEqual("True", loaded.CanOverwrite.ToString());
AssertEqual(OrganizationTargetMode.CustomRootNumberFolder.ToString(), loaded.Preferences.TargetMode.ToString());
AssertEqual(customRoot, loaded.Preferences.CustomRootDirectory);
@@ -299,6 +349,7 @@ static Task TestAppPreferencesStore()
AssertEqual("1", migrated.Preferences.RecentCustomRootDirectories.Length.ToString());
AssertEqual(secondRoot, migrated.Preferences.RecentCustomRootDirectories[0]);
AssertEqual("False", migrated.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), migrated.Preferences.CrossVolumeVerification.ToString());
AssertEqual(UiLanguageCodes.SimplifiedChinese, migrated.Preferences.UiLanguage);
var v2Json = System.Text.Json.JsonSerializer.Serialize(new
@@ -319,6 +370,7 @@ static Task TestAppPreferencesStore()
AssertEqual(AppPreferences.CurrentSchemaVersion.ToString(), migratedV2.Preferences.SchemaVersion.ToString());
AssertEqual("True", migratedV2.CanOverwrite.ToString());
AssertEqual("False", migratedV2.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), migratedV2.Preferences.CrossVolumeVerification.ToString());
AssertEqual(UiLanguageCodes.SimplifiedChinese, migratedV2.Preferences.UiLanguage);
var v3Json = System.Text.Json.JsonSerializer.Serialize(new
@@ -333,11 +385,27 @@ static Task TestAppPreferencesStore()
AssertEqual(AppPreferences.CurrentSchemaVersion.ToString(), migratedV3.Preferences.SchemaVersion.ToString());
AssertEqual(UiLanguageCodes.SimplifiedChinese, migratedV3.Preferences.UiLanguage);
AssertEqual("True", migratedV3.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), migratedV3.Preferences.CrossVolumeVerification.ToString());
+
+ var v4Json = System.Text.Json.JsonSerializer.Serialize(new
+ {
+ SchemaVersion = 4,
+ UiLanguage = "en",
+ RememberSavePreferences = true,
+ DirectSaveOverwrite = false,
+ TargetMode = "CustomRootNumberFolder",
+ CustomRootDirectory = customRoot
+ });
+ File.WriteAllText(store.SettingsPath, v4Json);
+ var migratedV4 = store.Load();
+ AssertEqual(AppPreferences.CurrentSchemaVersion.ToString(), migratedV4.Preferences.SchemaVersion.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), migratedV4.Preferences.CrossVolumeVerification.ToString());
File.WriteAllText(store.SettingsPath, "{ invalid json");
var malformed = store.Load();
AssertEqual("False", malformed.Preferences.RememberSavePreferences.ToString());
AssertEqual("False", malformed.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), malformed.Preferences.CrossVolumeVerification.ToString());
AssertEqual("True", malformed.CanOverwrite.ToString());
AssertEqual("True", (!string.IsNullOrWhiteSpace(malformed.Warning)).ToString());
@@ -354,6 +422,7 @@ static Task TestAppPreferencesStore()
var future = store.Load();
AssertEqual("False", future.Preferences.RememberSavePreferences.ToString());
AssertEqual("False", future.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), future.Preferences.CrossVolumeVerification.ToString());
AssertEqual("False", future.CanOverwrite.ToString());
AssertEqual("True", File.ReadAllText(store.SettingsPath).Contains("99", StringComparison.Ordinal).ToString());
@@ -361,13 +430,15 @@ static Task TestAppPreferencesStore()
{
UiLanguage = UiLanguageCodes.English,
RememberSavePreferences = false,
- DirectSaveOverwrite = true
+ DirectSaveOverwrite = true,
+ CrossVolumeVerification = CrossVolumeVerificationMode.FileSizeOnly
});
AssertEqual("True", File.Exists(store.SettingsPath).ToString());
var disabledMemory = store.Load();
AssertEqual(UiLanguageCodes.English, disabledMemory.Preferences.UiLanguage);
AssertEqual("False", disabledMemory.Preferences.RememberSavePreferences.ToString());
AssertEqual("False", disabledMemory.Preferences.DirectSaveOverwrite.ToString());
+ AssertEqual(CrossVolumeVerificationMode.FullSha256.ToString(), disabledMemory.Preferences.CrossVolumeVerification.ToString());
return Task.CompletedTask;
}
finally
@@ -974,6 +1045,50 @@ static async Task TestArtworkOutput()
}
}
+static async Task TestMissingSampleImagesAreSkipped()
+{
+ var root = Path.Combine(Path.GetTempPath(), $"JavMetaLite.NoSampleImagesTests.{Guid.NewGuid():N}");
+ Directory.CreateDirectory(root);
+ try
+ {
+ var videoPath = Path.Combine(root, "START-585.mp4");
+ await File.WriteAllBytesAsync(videoPath, [0x53, 0x54, 0x41, 0x52, 0x54]);
+ var metadata = new MovieMetadata
+ {
+ Id = "START-585",
+ Title = "无独立剧照测试",
+ ScreenshotUrls = []
+ };
+
+ var options = new JavMetaLite.Core.Models.SaveOptions(false, false, false, true, false);
+ using var service = new OutputService();
+ var result = await service.SaveAsync(
+ videoPath,
+ metadata,
+ options);
+
+ AssertEqual("0", result.ExtrafanartPaths.Count.ToString());
+ AssertEqual("False", Directory.Exists(Path.Combine(root, "extrafanart")).ToString());
+ AssertEqual("True", File.Exists(videoPath).ToString());
+
+ var organizationService = new FileOrganizationService(service);
+ var plan = FileOrganizationService.BuildPlan(
+ videoPath,
+ metadata,
+ options,
+ new OrganizationOptions(false, false));
+ var organized = await organizationService.ExecuteAsync(plan, metadata, allowOverwrite: false);
+ AssertEqual("0", organized.Outputs.ExtrafanartPaths.Count.ToString());
+ AssertEqual(videoPath, organized.VideoPath);
+ AssertEqual("False", organized.VideoMoved.ToString());
+ AssertEqual("True", File.Exists(videoPath).ToString());
+ }
+ finally
+ {
+ Directory.Delete(root, true);
+ }
+}
+
static async Task TestLocalCompleteCoverOutput()
{
var root = Path.Combine(Path.GetTempPath(), $"JavMetaLite.LocalCoverOutputTests.{Guid.NewGuid():N}");
diff --git a/JavMetaLite.UiSmokeTests/Program.cs b/JavMetaLite.UiSmokeTests/Program.cs
index f4ddc9b..66563f8 100644
--- a/JavMetaLite.UiSmokeTests/Program.cs
+++ b/JavMetaLite.UiSmokeTests/Program.cs
@@ -215,6 +215,12 @@ sourceComboBox.Items[0] is not ComboBoxItem languageAutoSource ||
{
throw new InvalidOperationException("完整封套尚未加载时应保留无文字的固定间距。 ");
}
+ if (window.FindName("DropHint") is not StackPanel dropHint ||
+ dropHint.Children.OfType().All(text =>
+ !text.Text.Contains("番号文件夹", StringComparison.Ordinal)))
+ {
+ throw new InvalidOperationException("拖放提示没有说明可直接拖入番号文件夹。 ");
+ }
var initialPosterPreviewBorder = window.FindName("PosterPreviewBorder") as Border
?? throw new InvalidOperationException("封套预览区域未创建。 ");
var fanartPreviewBorder = window.FindName("FanartPreviewBorder") as Border
@@ -255,6 +261,12 @@ sourceComboBox.Items[0] is not ComboBoxItem languageAutoSource ||
{
throw new InvalidOperationException("v0.8 安全偏好开关未创建或没有保持默认关闭。 ");
}
+ if (window.FindName("SkipCrossVolumeVerificationCheckBox") is not CheckBox skipVerificationCheckBox ||
+ skipVerificationCheckBox.IsChecked == true ||
+ skipVerificationCheckBox.Visibility != Visibility.Collapsed)
+ {
+ throw new InvalidOperationException("v1.1 跨盘校验选项未创建或没有保持完整校验默认值。 ");
+ }
var applyPreferences = typeof(MainWindow).GetMethod(
"ApplyPreferences",
@@ -272,6 +284,7 @@ sourceComboBox.Items[0] is not ComboBoxItem languageAutoSource ||
UiLanguage = UiLanguageCodes.SimplifiedChinese,
RememberSavePreferences = true,
DirectSaveOverwrite = true,
+ CrossVolumeVerification = CrossVolumeVerificationMode.FileSizeOnly,
TargetMode = OrganizationTargetMode.CustomRootNumberFolder,
CustomRootDirectory = preferencesRoot,
RecentCustomRootDirectories = [secondPreferencesRoot, preferencesRoot],
@@ -288,6 +301,7 @@ sourceComboBox.Items[0] is not ComboBoxItem languageAutoSource ||
if (directSaveCheckBox.IsChecked != true ||
rememberPreferencesCheckBox.IsChecked != true ||
!remembered.DirectSaveOverwrite ||
+ remembered.CrossVolumeVerification != CrossVolumeVerificationMode.FileSizeOnly ||
remembered.TargetMode != OrganizationTargetMode.CustomRootNumberFolder ||
remembered.CustomRootDirectory != preferencesRoot ||
!remembered.RenameVideo || remembered.WriteNfo || remembered.DownloadPoster ||
@@ -351,6 +365,10 @@ sourceComboBox.Items[0] is not ComboBoxItem languageAutoSource ||
{
throw new InvalidOperationException("v0.8.1 安全默认值没有关闭直接保存并覆盖。 ");
}
+ if (skipVerificationCheckBox.IsChecked == true)
+ {
+ throw new InvalidOperationException("v1.1 安全默认值没有恢复完整 SHA-256 校验。 ");
+ }
if (window.FindName("SaveButton") is not Button saveButton || saveButton.Content?.ToString() != "保存")
{
throw new InvalidOperationException("v0.4 保存入口未创建。 ");
@@ -586,10 +604,11 @@ posterImage.Source is null || fanartImage.Source is null ||
reviewedLocalMetadata.Director != "在线导演" ||
titleSourceText.Content?.ToString() != "LibreDMM ▾" ||
directorSourceText.Content?.ToString() != "LibreDMM ▾" ||
- artworkSourceButton.Content?.ToString() != "本地图片 ▾" ||
+ artworkSourceButton.Content?.ToString() != "LibreDMM ▾" ||
+ reviewedLocalMetadata.CoverUrl != localLibre.CoverUrl ||
posterImage.Source is null || fanartImage.Source is null)
{
- throw new InvalidOperationException("在线搜索后没有默认选择新文字资料,或意外改变了本地图片。 ");
+ throw new InvalidOperationException("在线搜索后没有统一默认选择新的文字与图片资料。 ");
}
artworkSourceButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
@@ -730,10 +749,20 @@ posterImage.Source is null || fanartImage.Source is null ||
customRootTextBox.Text = $@"{otherDrive}\JavMetaLite-dev2-test";
window.UpdateLayout();
if (!saveButton.IsEnabled ||
- !targetPathHintText.Text.Contains("安全复制 + SHA-256", StringComparison.Ordinal))
+ !targetPathHintText.Text.Contains("安全复制 + SHA-256", StringComparison.Ordinal) ||
+ skipVerificationCheckBox.Visibility != Visibility.Visible ||
+ skipVerificationCheckBox.IsChecked == true)
+ {
+ throw new InvalidOperationException("v1.1 跨盘符目标没有保持完整 SHA-256 默认值或显示校验选项。 ");
+ }
+ skipVerificationCheckBox.IsChecked = true;
+ window.UpdateLayout();
+ if (!targetPathHintText.Text.Contains("快速跨盘复制", StringComparison.Ordinal) ||
+ !targetPathHintText.Text.Contains("风险自负", StringComparison.Ordinal))
{
- throw new InvalidOperationException("dev3 跨盘符目标没有启用安全复制提示。 ");
+ throw new InvalidOperationException("v1.1 快速跨盘模式没有显示明确风险提示。 ");
}
+ skipVerificationCheckBox.IsChecked = false;
}
targetModeComboBox.SelectedIndex = 1;
@@ -783,7 +812,8 @@ posterImage.Source is null || fanartImage.Source is null ||
new PlannedFileChange(PlannedChangeKind.UpdateFile, "更新 NFO", "C:\\Media\\IPX-123\\IPX-123.nfo"),
new PlannedFileChange(PlannedChangeKind.KeepFile, "poster 内容保持不变", "C:\\Media\\IPX-123\\IPX-123-poster.jpg"),
new PlannedFileChange(PlannedChangeKind.ReplaceImage, "替换 fanart", "C:\\Media\\IPX-123\\IPX-123-fanart.jpg"),
- new PlannedFileChange(PlannedChangeKind.CopyAndVerifyVideo, "安全复制影片", "D:\\Media\\IPX-123\\IPX-123.mp4", "C:\\Media\\source.mp4")
+ new PlannedFileChange(PlannedChangeKind.CopyAndVerifyVideo, "安全复制影片", "D:\\Media\\IPX-123\\IPX-123.mp4", "C:\\Media\\source.mp4"),
+ new PlannedFileChange(PlannedChangeKind.CopyVideo, "快速复制影片", "E:\\Media\\IPX-123\\IPX-123.mp4", "C:\\Media\\source.mp4")
],
[],
[]);
@@ -816,7 +846,7 @@ posterImage.Source is null || fanartImage.Source is null ||
var previewActions = previewChanges.Items.Cast