Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0cce61f
Use bind operator
codeconscious Apr 18, 2026
a12f158
Refactor createSets func
codeconscious Apr 18, 2026
7a2d553
Update package; use List.ensureOneV
codeconscious Apr 18, 2026
ad8fbf8
Update package; use List.ensureNotEmptyV
codeconscious Apr 18, 2026
a672c6d
Add validation CE
codeconscious Apr 18, 2026
66dca69
Restore Validation.map3 due to 2 failing tests
codeconscious Apr 18, 2026
b0bb31f
Fix validation CE
codeconscious Apr 19, 2026
f765080
Use FileInfo in History
codeconscious Apr 21, 2026
9a5bb53
Update CCFSharpUtils package version
codeconscious Apr 22, 2026
7be4513
Minor naming and layout
codeconscious Apr 22, 2026
d411f9b
Update CCFSharpUtils; update imports
codeconscious Apr 24, 2026
0f4b322
Erase Validation.map3
codeconscious May 26, 2026
63c90f7
Move 'open'
codeconscious May 26, 2026
dbb4a2e
Remove empty line
codeconscious May 26, 2026
ceb4da8
Add 'no rename pattern matches' message
codeconscious May 26, 2026
958ac0e
Add return type
codeconscious May 27, 2026
bab97a0
Updates to updateTextViaPatterns
codeconscious May 27, 2026
9c518ff
Refactor updateTextViaPatterns
codeconscious May 27, 2026
9a0fce3
Replace List.isEmpty with pattern matching
codeconscious May 27, 2026
86cb38c
Remove unneeded no-match messages during renaming
codeconscious May 28, 2026
985048e
Add func comment
codeconscious May 28, 2026
e88cecc
Revert pattern matching
codeconscious May 28, 2026
c9e6aa5
Combine code to one line
codeconscious May 28, 2026
f6076d9
Move summary printing to func
codeconscious May 28, 2026
78fdfe9
Convert value to func
codeconscious May 29, 2026
0ead773
Rename var
codeconscious May 29, 2026
699d838
Revert match call
codeconscious May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CCVTAC.Main/CCVTAC.Main.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CCFSharpUtils" Version="0.3.5.1" />
<PackageReference Include="CCFSharpUtils" Version="0.4.0" />
<PackageReference Include="CodeConscious.Startwatch" Version="1.0.0" />
<PackageReference Include="FSharpPlus" Version="1.9.1" />
<PackageReference Include="FsToolkit.ErrorHandling" Version="5.2.0" />
Expand Down
2 changes: 1 addition & 1 deletion src/CCVTAC.Main/Commands.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace CCVTAC.Main

open CCFSharpUtils
open CCFSharpUtils.Text
open System

module Commands =
Expand Down
2 changes: 1 addition & 1 deletion src/CCVTAC.Main/Downloading/Downloading.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace CCVTAC.Main.Downloading

open CCFSharpUtils
open CCFSharpUtils.Text

module Downloading =

Expand Down
2 changes: 1 addition & 1 deletion src/CCVTAC.Main/Downloading/Updater.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace CCVTAC.Main.Downloading
open CCVTAC.Main
open CCVTAC.Main.ExternalTools
open CCVTAC.Main.Settings.Settings
open CCFSharpUtils
open CCFSharpUtils.Text

module Updater =

Expand Down
3 changes: 2 additions & 1 deletion src/CCVTAC.Main/ExternalTools/Runner.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace CCVTAC.Main.ExternalTools

open CCVTAC.Main
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open Startwatch.Library
open System
open System.Diagnostics
Expand Down
11 changes: 6 additions & 5 deletions src/CCVTAC.Main/History.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace CCVTAC.Main

open CCVTAC.Main.IoUtilities.Files
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open System
open System.IO
open System.Text.Json
Expand All @@ -11,7 +12,7 @@ type History(filePath: string, displayCount: int) =

let separator = ';'

member private _.FilePath = filePath
member private _.FileInfo = FileInfo filePath
member private _.DisplayCount = displayCount

/// Write a URL and its related data to the history file.
Expand All @@ -20,16 +21,16 @@ type History(filePath: string, displayCount: int) =
let serializedTime = JsonSerializer.Serialize(entryTime).Replace("\"", "")
let text = serializedTime + string separator + url + String.newLine

match appendToFile this.FilePath text with
match appendToFile this.FileInfo text with
| Ok _ -> printer.Debug $"Added \"%s{url}\" to the history log."
| Error err -> printer.Error $"Failed to write \"%s{url}\" to the history log at \"{this.FilePath}\": {err}"
| Error err -> printer.Error $"Failed to write \"%s{url}\" to the history log at \"{this.FileInfo}\": {err}"
with exn ->
printer.Error $"Could not append URL(s) to history log: {exn.Message}"

member this.ShowRecent(printer: Printer) : unit =
try
let lines =
File.ReadAllLines this.FilePath
File.ReadAllLines this.FileInfo.FullName
|> Seq.takeLast this.DisplayCount
|> Seq.toList

Expand Down
6 changes: 2 additions & 4 deletions src/CCVTAC.Main/InputHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,11 @@ module InputHelper =
inputs
|> List.map (fun input ->
{ Text = input
Category = if input[0] = Commands.prefix
then InputCategory.Command
else InputCategory.Url })
Category = if input[0] = Commands.prefix then Command else Url })

let countCategories (inputs: CategorizedInput list) : CategoryCounts =
inputs
|> List.groupBy _.Category
|> List.map (fun (k, grp) -> k, grp |> Seq.length)
|> List.map (fun (category, items) -> category, Seq.length items)
|> Map.ofSeq
|> CategoryCounts
3 changes: 2 additions & 1 deletion src/CCVTAC.Main/IoUtilities/Directories.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace CCVTAC.Main.IoUtilities

open CCVTAC.Main
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open System.IO
open System.Text

Expand Down Expand Up @@ -96,4 +98,3 @@ module Directories =
|> Ok
with exn ->
Error $"Error accessing or creating directory \"%s{dirName}\": %s{exn.Message}"

5 changes: 3 additions & 2 deletions src/CCVTAC.Main/IoUtilities/Files.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace CCVTAC.Main.IoUtilities

open CCFSharpUtils
open CCFSharpUtils.Text
open System.IO

module Files =
Expand All @@ -18,5 +19,5 @@ module Files =
let readAllText (filePath: string) : Result<string, string> =
ofTry (fun _ -> File.ReadAllText filePath)

let appendToFile (filePath: string) (text: string) : Result<unit, string> =
ofTry (fun _ -> File.AppendAllText(filePath, text))
let appendToFile (file: FileInfo) (text: string) : Result<unit, string> =
ofTry (fun _ -> File.AppendAllText(file.FullName, text))
8 changes: 5 additions & 3 deletions src/CCVTAC.Main/Orchestrator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ open CCVTAC.Main.PostProcessing
open CCVTAC.Main.Settings
open CCVTAC.Main.Settings.Settings
open CCVTAC.Main.Settings.Settings.LiveUpdating
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open Startwatch.Library
open System

Expand Down Expand Up @@ -292,9 +293,10 @@ module Orchestrator =
let input = printer.GetInput prompt
let splitInputs = splitInputText input

if List.isEmpty splitInputs then
match splitInputs with
| [] ->
printer.Error $"Invalid input. Enter only URLs or commands beginning with \"%c{Commands.prefix}\"."
else
| _ ->
let categorizedInputs = categorizeInputs splitInputs
let categoryCounts = countCategories categorizedInputs
summarizeInput categorizedInputs categoryCounts printer
Expand Down
2 changes: 1 addition & 1 deletion src/CCVTAC.Main/PostProcessing/Deleter.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace CCVTAC.Main.PostProcessing

open CCVTAC.Main
open CCFSharpUtils
open CCFSharpUtils.Text
open System.IO

module Deleter =
Expand Down
3 changes: 1 addition & 2 deletions src/CCVTAC.Main/PostProcessing/MetadataUtilities.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace CCVTAC.Main.PostProcessing

open CCVTAC.Main
open CCFSharpUtils
open CCFSharpUtils.Text
open System
open System.Text

Expand Down
2 changes: 2 additions & 0 deletions src/CCVTAC.Main/PostProcessing/Mover.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ open CCVTAC.Main.PostProcessing
open CCVTAC.Main.PostProcessing.Tagging
open CCVTAC.Main.Settings.Settings
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open TaggingSet
open System
open System.IO
Expand Down
3 changes: 2 additions & 1 deletion src/CCVTAC.Main/PostProcessing/PostProcessing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ open CCVTAC.Main.IoUtilities
open CCVTAC.Main.PostProcessing.Tagging
open CCVTAC.Main.PostProcessing.Tagging.TaggingSet
open CCVTAC.Main.Settings.Settings
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open Startwatch.Library
open System.IO
open System.Linq
Expand Down
69 changes: 30 additions & 39 deletions src/CCVTAC.Main/PostProcessing/Renamer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ open CCVTAC.Main
open CCVTAC.Main.IoUtilities
open CCVTAC.Main.Settings.Settings
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open System
open System.IO
open System.Text
Expand All @@ -19,50 +21,39 @@ module Renamer =
| "KC" -> NormalizationForm.FormKC
| _ -> NormalizationForm.FormC

let updateTextViaPatterns isQuietMode (printer: Printer) (sb: StringBuilder) (renamePattern: RenamePattern) =
let updateTextViaPattern isQuietMode (printer: Printer) (text: SB) (renamePattern: RenamePattern) : SB =
let regex = Regex renamePattern.RegexPattern

let matches =
regex.Matches(sb.ToString())
|> Rgx.successMatches
|> Seq.rev
|> Seq.toList
let matches = text.ToString() |> regex.Matches |> Rgx.successMatches |> Seq.rev |> Seq.toList

if List.isEmpty matches
then sb
else
if not isQuietMode then
let patternSummary =
if String.hasText renamePattern.Summary then
$"\"%s{renamePattern.Summary}\""
else
$"`%s{renamePattern.RegexPattern}` (no description)"
let printSummary () =
let patternDesc = if String.hasText renamePattern.Summary
then $"\"%s{renamePattern.Summary}\""
else $"`%s{renamePattern.RegexPattern}` (no description)"
printer.Debug $"> Rename pattern %s{patternDesc} matched (%d{matches.Length}×)."

printer.Debug $"Rename pattern %s{patternSummary} matched × %d{matches.Length}."
/// Builds replacement text by substituting %<n>s placeholders with captured regex group values.
/// Group values are trimmed, and indexing starts from 1 (because group 0 is the full match).
let buildReplacementText (renamePattern: RenamePattern) (m: Match) : string =
let sb = SB renamePattern.ReplaceWithPattern

for m in matches do
sb.Remove(m.Index, m.Length) |> ignore

// Build replacement text by replacing %<n> placeholders with group captures.
let replacementText =
m
|> Rgx.groups
|> Seq.mapi (fun i _ ->
let searchFor = sprintf "%%<%d>s" (i + 1)
let replaceWith =
// Group 0 is the entire match, so we only want groups starting at 1.
if i + 1 < m.Groups.Count
then m.Groups[i + 1].Value.Trim()
else String.Empty
(searchFor, replaceWith))
|> Seq.fold
(fun (sb': StringBuilder) -> sb'.Replace)
(StringBuilder renamePattern.ReplaceWithPattern)
|> _.ToString()
for i = 1 to m.Groups.Count - 1 do
let placeholder = sprintf "%%<%d>s" i
let value = if i < m.Groups.Count then m.Groups[i].Value.Trim() else String.Empty
sb.Replace(placeholder, value) |> ignore

sb.ToString()

sb.Insert(m.Index, replacementText) |> ignore
if List.isEmpty matches then
text
else
if not isQuietMode then printSummary ()

for m in matches do
let replacementText = buildReplacementText renamePattern m
text.Remove(m.Index, m.Length).Insert(m.Index, replacementText) |> ignore

sb
text

let run userSettings workingDirectory (printer: Printer) : unit =
let watch = Watch()
Expand All @@ -82,8 +73,8 @@ module Renamer =
let newFileName =
userSettings.RenamePatterns
|> List.fold
(fun (sb: StringBuilder) -> updateTextViaPatterns userSettings.QuietMode printer sb)
(StringBuilder audioFile.Name)
(fun acc -> updateTextViaPattern userSettings.QuietMode printer acc)
(SB audioFile.Name)
|> _.ToString()

try
Expand Down
4 changes: 2 additions & 2 deletions src/CCVTAC.Main/PostProcessing/Tagging/Tagger.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ open CCVTAC.Main.Settings.Settings
open CCVTAC.Main.PostProcessing
open CCVTAC.Main.PostProcessing.Tagging
open CCVTAC.Main.Downloading.Downloading
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Text
open Startwatch.Library
open TaggingSet
open MetadataUtilities
open System
open System.IO
Expand Down
57 changes: 24 additions & 33 deletions src/CCVTAC.Main/PostProcessing/Tagging/TaggingSet.fs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
namespace CCVTAC.Main.PostProcessing.Tagging

open CCVTAC.Main.IoUtilities
open CCFSharpUtils
open CCFSharpUtils.Collections
open CCFSharpUtils.Operators
open CCFSharpUtils.Text
open FsToolkit.ErrorHandling
open System.IO
open System.Text.RegularExpressions
Expand Down Expand Up @@ -32,17 +33,6 @@ module TaggingSet =
ImageFile = i }

let private createValidated (videoId, fileNames) : Result<TaggingSet, string list> =
let ensureNotEmpty xs errorMsg : Validation<'a list, string> =
if List.isNotEmpty xs
then Ok xs
else Error [errorMsg]

let ensureExactlyOne xs emptyErrorMsg multipleErrorMsg : Validation<'a, string> =
match xs with
| [] -> Error [emptyErrorMsg]
| [x] -> Ok x
| _ -> Error [multipleErrorMsg]

let hasSupportedAudioExt (fileName: string) =
match Path.GetExtension fileName with
| Null -> false
Expand All @@ -55,39 +45,40 @@ module TaggingSet =
Files.imageFileExts
|> List.collect (fun ext -> fileNames |> Files.filterByExt ext)

Validation.map3
(fun a j i -> create videoId a j i)
(ensureNotEmpty audioFiles
$"No supported audio files found for video ID {videoId}.")
(ensureExactlyOne jsonFiles
$"No JSON file found for video ID {videoId}."
$"Multiple JSON files found for video ID {videoId}.")
(ensureExactlyOne imageFiles
$"No image file found for video ID {videoId}."
$"Multiple image files found for video ID {videoId}.")
validation {
let! a = List.ensureNotEmptyV audioFiles
$"No supported audio files found for video ID {videoId}."
and! j = List.ensureOneV jsonFiles
$"No JSON file found for video ID {videoId}."
$"Multiple JSON files found for video ID {videoId}."
and! i = List.ensureOneV imageFiles
$"No image file found for video ID {videoId}."
$"Multiple image files found for video ID {videoId}."
return create videoId a j i
}

/// Creates a collection of TaggingSets from a collection of file paths related to several video IDs.
/// Any extra, unnecessary files will be ignored.
/// Any validation errors will be accumulated and return in an Error.
/// Creates a collection of TaggingSets from a collection of file paths related to video IDs.
/// Irrelevant files are ignored. Validation errors are accumulated and returned in an Error.
let createSets filePaths : Result<TaggingSet list, string list> =
if Seq.isEmpty filePaths then
Error ["No file paths to create a tagging set were provided."]
else
let isRelevantFile fileName : Match option =
let relevantFileInfo fileName =
// Regex group 0 is the full filename, and group 1 contains the video ID.
let fileNamesHavingVideoIdsRgx =
Regex(@".+\[([\w_\-]{11})\](?:.*)?\.(\w+)", RegexOptions.Compiled)

fileName |> Rgx.trySuccessMatch fileNamesHavingVideoIdsRgx

let fileName (m: Match) = m.Groups[0].Value
let videoId (m: Match) = m.Groups[1].Value
fileName
|> Rgx.trySuccessMatch fileNamesHavingVideoIdsRgx
|> Option.map (fun m ->
{| FileName = m.Groups[0].Value
VideoId = m.Groups[1].Value |} )

filePaths
|> List.ofSeq
|> List.choose isRelevantFile
|> List.groupBy videoId
|> List.mapSnd fileName
|> List.choose relevantFileInfo
|> List.groupBy _.VideoId
|> List.mapSnd _.FileName
|> List.map createValidated
|> List.sequenceResultA
|!! List.collect id
1 change: 1 addition & 0 deletions src/CCVTAC.Main/Printer.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace CCVTAC.Main

open CCFSharpUtils
open CCFSharpUtils.Text
open System
open System.Collections.Generic
open System.Linq
Expand Down
Loading