Compare commits
11 Commits
2c90337546
...
1c1f30a297
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c1f30a297 | |||
|
|
877d4d941f | ||
|
|
5269f28c41 | ||
|
|
66756b555f | ||
|
|
d784357fe5 | ||
| 4f021d5a3e | |||
| e33e985031 | |||
|
|
20f3709ac9 | ||
|
|
358cc33f3a | ||
|
|
321f037fa2 | ||
|
|
d0b03bfe77 |
13
.idea/.idea.BangumiRenamer.dir/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.BangumiRenamer.dir/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider 忽略的文件
|
||||||
|
/modules.xml
|
||||||
|
/contentModel.xml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
/.idea.BangumiRenamer.iml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
10
.idea/.idea.BangumiRenamer.dir/.idea/indexLayout.xml
generated
Normal file
10
.idea/.idea.BangumiRenamer.dir/.idea/indexLayout.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes>
|
||||||
|
<Path>Test</Path>
|
||||||
|
</explicitExcludes>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.BangumiRenamer.dir/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.BangumiRenamer.dir/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -8,9 +8,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<EmbeddedResource Include="Res/**" />
|
||||||
<PackageReference Include="OllamaSharp" Version="5.1.14" />
|
</ItemGroup>
|
||||||
<PackageReference Include="TMDbLib" Version="2.2.0" />
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Res\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LLCSharpUtils" Version="1.0.2" />
|
||||||
|
<PackageReference Include="NativeFileDialogSharp" Version="0.5.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
|
<PackageReference Include="TMDbLib" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
115
EpisodeGroup.cs
115
EpisodeGroup.cs
@ -1,115 +0,0 @@
|
|||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public string spot;
|
|
||||||
public List<Node> son;
|
|
||||||
public string session;
|
|
||||||
public string title;
|
|
||||||
public bool isOverride;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EpisodeGroup
|
|
||||||
{
|
|
||||||
private Node _root;
|
|
||||||
|
|
||||||
public readonly List<EpisodeInfo> episodes = new List<EpisodeInfo>();
|
|
||||||
|
|
||||||
private Node FindOrCreateShow(Node node, string spot)
|
|
||||||
{
|
|
||||||
if (node.son == null)
|
|
||||||
{
|
|
||||||
node.son = new List<Node>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Node target = null;
|
|
||||||
foreach (var son in node.son)
|
|
||||||
{
|
|
||||||
if (son.spot == spot)
|
|
||||||
{
|
|
||||||
target = son;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target == null)
|
|
||||||
{
|
|
||||||
target = new Node
|
|
||||||
{
|
|
||||||
spot = spot,
|
|
||||||
isOverride = false
|
|
||||||
};
|
|
||||||
node.son.Add(target);
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Add(EpisodeInfo episode)
|
|
||||||
{
|
|
||||||
if (_root == null)
|
|
||||||
{
|
|
||||||
_root = new Node
|
|
||||||
{
|
|
||||||
spot = "",
|
|
||||||
isOverride = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var curr = _root;
|
|
||||||
var spots = episode.path.Split('/');
|
|
||||||
foreach (var spot in spots)
|
|
||||||
{
|
|
||||||
curr = FindOrCreateShow(curr, spot);
|
|
||||||
}
|
|
||||||
curr.session = episode.session;
|
|
||||||
curr.title = episode.name;
|
|
||||||
curr.isOverride = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
_root = null;
|
|
||||||
foreach (var episode in episodes)
|
|
||||||
{
|
|
||||||
Add(episode);
|
|
||||||
}
|
|
||||||
|
|
||||||
DoRun(_root);
|
|
||||||
|
|
||||||
foreach (var episode in episodes)
|
|
||||||
{
|
|
||||||
var curr = _root;
|
|
||||||
var spots = episode.path.Split('/');
|
|
||||||
foreach (var spot in spots)
|
|
||||||
{
|
|
||||||
curr = FindOrCreateShow(curr, spot);
|
|
||||||
if (curr.isOverride)
|
|
||||||
{
|
|
||||||
episode.name = curr.title;
|
|
||||||
episode.session = curr.session;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoRun(Node node)
|
|
||||||
{
|
|
||||||
if (node == null) return;
|
|
||||||
if (node.son == null) return;
|
|
||||||
foreach (var son in node.son)
|
|
||||||
{
|
|
||||||
DoRun(son);
|
|
||||||
}
|
|
||||||
var query = (from son in node.son
|
|
||||||
where son.isOverride
|
|
||||||
select son).GroupBy(node => (node.title, node.session));
|
|
||||||
foreach (var group in query)
|
|
||||||
{
|
|
||||||
if (group.Count() * 2 > node.son.Count)
|
|
||||||
{
|
|
||||||
node.isOverride = true;
|
|
||||||
(node.title, node.session) = group.Key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
146
EpisodeParser.cs
146
EpisodeParser.cs
@ -1,146 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public class EpisodeInfo
|
|
||||||
{
|
|
||||||
public string path;
|
|
||||||
public string name;
|
|
||||||
public string session;
|
|
||||||
public string episode;
|
|
||||||
public string group;
|
|
||||||
public string type; // others, episode, subtitle
|
|
||||||
public string language;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EpisodeParseResult
|
|
||||||
{
|
|
||||||
public bool success;
|
|
||||||
public string originalQuestion;
|
|
||||||
public EpisodeInfo parseResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EpisodeParser
|
|
||||||
{
|
|
||||||
// todo: 添加解析年份
|
|
||||||
private bool _running = false;
|
|
||||||
|
|
||||||
private const string PromptPath = "Prompt.txt";
|
|
||||||
|
|
||||||
private readonly string _prompt;
|
|
||||||
private readonly OllamaHelper _ollama;
|
|
||||||
|
|
||||||
private ConcurrentQueue<string> _questions;
|
|
||||||
private ConcurrentQueue<EpisodeParseResult> _results;
|
|
||||||
|
|
||||||
public bool Running => _running;
|
|
||||||
public int TotalQuestions => _questions.Count + _results.Count;
|
|
||||||
public int CompletedQuestions => _results.Count;
|
|
||||||
|
|
||||||
public int RestQuestions => _questions.Count;
|
|
||||||
|
|
||||||
public bool TryGetResult(out EpisodeParseResult result)
|
|
||||||
{
|
|
||||||
return _results.TryDequeue(out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EpisodeParser()
|
|
||||||
{
|
|
||||||
_prompt = File.ReadAllText(PromptPath);
|
|
||||||
_ollama = new OllamaHelper();
|
|
||||||
_questions = new ConcurrentQueue<string>();
|
|
||||||
_results = new ConcurrentQueue<EpisodeParseResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Append(string question)
|
|
||||||
{
|
|
||||||
_questions.Enqueue(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (_running) return ;
|
|
||||||
_running = true;
|
|
||||||
DoParse();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string Preprocess(string respoonds)
|
|
||||||
{
|
|
||||||
return respoonds.Replace("```json\n", "").Replace("```", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string RemoveNonDigits(string s)
|
|
||||||
{
|
|
||||||
return Regex.Replace(s, @"[^\d\.]*", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string RemoveFrontZeros(string s)
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
bool front = true;
|
|
||||||
foreach (var c in s)
|
|
||||||
{
|
|
||||||
if (front && c == '0') continue;
|
|
||||||
front = false;
|
|
||||||
result.Append(c);
|
|
||||||
}
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ProcessSession(string session)
|
|
||||||
{
|
|
||||||
session = RemoveFrontZeros(RemoveNonDigits(session));
|
|
||||||
if (session.Length > 2) session = "";
|
|
||||||
return session == "" ? "1" : session;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ProcessEpisode(string episode)
|
|
||||||
{
|
|
||||||
episode = RemoveFrontZeros(RemoveNonDigits(episode));
|
|
||||||
return episode == "" ? "1" : episode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FinalProcess(EpisodeInfo info)
|
|
||||||
{
|
|
||||||
if (info.type == "others")
|
|
||||||
{
|
|
||||||
info.episode = "";
|
|
||||||
info.session = ProcessSession(info.session);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
info.episode = ProcessEpisode(info.episode);
|
|
||||||
info.session = ProcessSession(info.session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void DoParse()
|
|
||||||
{
|
|
||||||
while (_questions.TryDequeue(out string question))
|
|
||||||
{
|
|
||||||
var result = new EpisodeParseResult();
|
|
||||||
result.originalQuestion = question;
|
|
||||||
var responds = await _ollama.Ask(_prompt + $"\"{question}\"");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
responds = Preprocess(responds);
|
|
||||||
result.parseResult = JsonConvert.DeserializeObject<EpisodeInfo>(responds);
|
|
||||||
result.parseResult.path = question;
|
|
||||||
result.success = result.parseResult != null;
|
|
||||||
if (result.success)
|
|
||||||
{
|
|
||||||
FinalProcess(result.parseResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception _)
|
|
||||||
{
|
|
||||||
result.success = false;
|
|
||||||
}
|
|
||||||
_results.Enqueue(result);
|
|
||||||
}
|
|
||||||
_running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using OllamaSharp;
|
|
||||||
|
|
||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public class OllamaHelper
|
|
||||||
{
|
|
||||||
private const string SelectedModel = "gemma3:12b";
|
|
||||||
private readonly OllamaApiClient _ollama;
|
|
||||||
|
|
||||||
public OllamaHelper()
|
|
||||||
{
|
|
||||||
var uri = new Uri("http://localhost:11434");
|
|
||||||
_ollama = new OllamaApiClient(uri);
|
|
||||||
_ollama.SelectedModel = SelectedModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> Ask(string question)
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
await foreach (var stream in _ollama.GenerateAsync(question))
|
|
||||||
result.Append(stream.Response);
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public static class PathExtension
|
|
||||||
{
|
|
||||||
public static string ToUnixPath(this string path)
|
|
||||||
{
|
|
||||||
return path.Replace(@"\", "/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
174
Playgournd.cs
174
Playgournd.cs
@ -1,174 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public static class Playgournd
|
|
||||||
{
|
|
||||||
public static void GetAllFiles(string basePath = @"\\192.168.31.10\media\downloads\aria2\TV\")
|
|
||||||
{
|
|
||||||
var files = Directory.GetFiles(basePath, "*", SearchOption.AllDirectories);
|
|
||||||
|
|
||||||
files = files.Select(path => path.Replace(basePath, "").ToUnixPath()).ToArray();
|
|
||||||
|
|
||||||
|
|
||||||
var result = new StringBuilder();
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
result.AppendLine(file);
|
|
||||||
}
|
|
||||||
File.WriteAllText("questions.txt", result.ToString(), Encoding.UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ParseQuestions(string path = "questions.txt")
|
|
||||||
{
|
|
||||||
var parser = new EpisodeParser();
|
|
||||||
var questions = File.ReadAllLines(path);
|
|
||||||
foreach (var question in questions)
|
|
||||||
{
|
|
||||||
parser.Append(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.Start();
|
|
||||||
int current = -1;
|
|
||||||
var stopwatch = new Stopwatch();
|
|
||||||
stopwatch.Start();
|
|
||||||
while (parser.Running)
|
|
||||||
{
|
|
||||||
if (current != parser.CompletedQuestions)
|
|
||||||
{
|
|
||||||
var prompt = $"{parser.CompletedQuestions}/{parser.TotalQuestions}";
|
|
||||||
if (current != -1)
|
|
||||||
{
|
|
||||||
prompt += $", 预计剩余 {stopwatch.Elapsed.Seconds * parser.RestQuestions}s";
|
|
||||||
stopwatch.Restart();
|
|
||||||
}
|
|
||||||
current = parser.CompletedQuestions;
|
|
||||||
Console.WriteLine(prompt);
|
|
||||||
}
|
|
||||||
await Task.Delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
var grouper = new EpisodeGroup();
|
|
||||||
|
|
||||||
while (parser.TryGetResult(out var result))
|
|
||||||
{
|
|
||||||
if (!result.success)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"解析失败: {result.originalQuestion}");
|
|
||||||
}
|
|
||||||
grouper.episodes.Add(result.parseResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
grouper.Run();
|
|
||||||
|
|
||||||
File.WriteAllText($"results_{DateTime.Now:yyyy_MM_dd-HH_mm_ss}.json", JsonConvert.SerializeObject(grouper.episodes, Formatting.Indented), Encoding.UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ReparseFailedQuestions(string path)
|
|
||||||
{
|
|
||||||
var parser = new EpisodeParser();
|
|
||||||
var resultsJson = File.ReadAllText(path);
|
|
||||||
var results = JsonConvert.DeserializeObject<List<EpisodeParseResult>>(resultsJson);
|
|
||||||
var dict = new Dictionary<string, EpisodeParseResult>();
|
|
||||||
|
|
||||||
for (int i = 0; i < results.Count; i++)
|
|
||||||
{
|
|
||||||
var result = results[i];
|
|
||||||
if (result.success == false)
|
|
||||||
{
|
|
||||||
dict[result.originalQuestion] = result;
|
|
||||||
parser.Append(result.originalQuestion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parser.Start();
|
|
||||||
int current = -1;
|
|
||||||
var stopwatch = new Stopwatch();
|
|
||||||
stopwatch.Start();
|
|
||||||
while (parser.Running)
|
|
||||||
{
|
|
||||||
if (current != parser.CompletedQuestions)
|
|
||||||
{
|
|
||||||
var prompt = $"{parser.CompletedQuestions}/{parser.TotalQuestions}";
|
|
||||||
if (current != -1)
|
|
||||||
{
|
|
||||||
prompt += $", 预计剩余 {stopwatch.Elapsed.Seconds * parser.RestQuestions}s";
|
|
||||||
stopwatch.Restart();
|
|
||||||
}
|
|
||||||
current = parser.CompletedQuestions;
|
|
||||||
Console.WriteLine(prompt);
|
|
||||||
}
|
|
||||||
await Task.Delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parser.TryGetResult(out var result))
|
|
||||||
{
|
|
||||||
dict[result.originalQuestion].success = result.success;
|
|
||||||
dict[result.originalQuestion].parseResult = result.parseResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllText($"results_{DateTime.Now:yyyy_MM_dd-HH_mm_ss}.json", JsonConvert.SerializeObject(results), Encoding.UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<ShowsManager> CalcShows(string path)
|
|
||||||
{
|
|
||||||
var resultsJson = File.ReadAllText(path);
|
|
||||||
var results = JsonConvert.DeserializeObject<List<EpisodeParseResult>>(resultsJson);
|
|
||||||
var showsManager = new ShowsManager();
|
|
||||||
foreach (var result in results)
|
|
||||||
{
|
|
||||||
showsManager.AppendEpisode(result.parseResult);
|
|
||||||
}
|
|
||||||
return showsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task Repair(string path, string qPath)
|
|
||||||
{
|
|
||||||
var parser = new EpisodeParser();
|
|
||||||
var questions = File.ReadAllLines(qPath);
|
|
||||||
|
|
||||||
var resultsJson = File.ReadAllText(path);
|
|
||||||
var results = JsonConvert.DeserializeObject<List<EpisodeParseResult>>(resultsJson);
|
|
||||||
var dict = new Dictionary<string, EpisodeParseResult>();
|
|
||||||
|
|
||||||
for (int i = 0; i < results.Count; i++)
|
|
||||||
{
|
|
||||||
var result = results[i];
|
|
||||||
if (questions.Contains(result.originalQuestion))
|
|
||||||
{
|
|
||||||
dict[result.originalQuestion] = result;
|
|
||||||
parser.Append(result.originalQuestion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.Start();
|
|
||||||
int current = -1;
|
|
||||||
var stopwatch = new Stopwatch();
|
|
||||||
stopwatch.Start();
|
|
||||||
while (parser.Running)
|
|
||||||
{
|
|
||||||
if (current != parser.CompletedQuestions)
|
|
||||||
{
|
|
||||||
var prompt = $"{parser.CompletedQuestions}/{parser.TotalQuestions}";
|
|
||||||
if (current != -1)
|
|
||||||
{
|
|
||||||
prompt += $", 预计剩余 {stopwatch.Elapsed.Seconds * parser.RestQuestions}s";
|
|
||||||
stopwatch.Restart();
|
|
||||||
}
|
|
||||||
current = parser.CompletedQuestions;
|
|
||||||
Console.WriteLine(prompt);
|
|
||||||
}
|
|
||||||
await Task.Delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parser.TryGetResult(out var result))
|
|
||||||
{
|
|
||||||
dict[result.originalQuestion].success = result.success;
|
|
||||||
dict[result.originalQuestion].parseResult = result.parseResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllText($"results_{DateTime.Now:yyyy_MM_dd-HH_mm_ss}.json", JsonConvert.SerializeObject(results), Encoding.UTF8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
Program.cs
10
Program.cs
@ -1,10 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using ConsoleApp1;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
await Playgournd.ParseQuestions();
|
|
||||||
|
|
||||||
// var shows = new ShowsManager();
|
|
||||||
// shows.AppendEpisodeFromFile("results_2025_05_13-01_14_16.json");
|
|
||||||
// await shows.QueryTMDB(new Dictionary<string, string> {{"The Name of the People", "人民的名义"}});
|
|
||||||
// shows.MoveFiles(@"\\192.168.31.10\media\downloads\aria2\TV", @"\\192.168.31.10\media\downloads\aria2\Done");
|
|
||||||
226
ShowsManager.cs
226
ShowsManager.cs
@ -1,226 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using TMDbLib.Client;
|
|
||||||
|
|
||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public class ShowSession
|
|
||||||
{
|
|
||||||
public string session;
|
|
||||||
public List<EpisodeInfo> extras = new List<EpisodeInfo>();
|
|
||||||
public List<EpisodeInfo> episodes = new List<EpisodeInfo>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Show
|
|
||||||
{
|
|
||||||
public string rawTitle;
|
|
||||||
public string title;
|
|
||||||
public string year;
|
|
||||||
public string tmdbId;
|
|
||||||
public List<ShowSession> sessions = new List<ShowSession>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ShowsManager
|
|
||||||
{
|
|
||||||
private List<Show> _shows = new List<Show>();
|
|
||||||
|
|
||||||
private TMDbClient _client;
|
|
||||||
|
|
||||||
public ShowsManager()
|
|
||||||
{
|
|
||||||
_client = new TMDbClient("991107af25913562cfa06622a52873e1", proxy: new WebProxy("http://127.0.0.1:7897"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Show FindOrCreateShow(List<Show> shows, string rawTitle)
|
|
||||||
{
|
|
||||||
foreach (var show in shows)
|
|
||||||
{
|
|
||||||
if (show.rawTitle == rawTitle)
|
|
||||||
{
|
|
||||||
return show;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new Show();
|
|
||||||
result.rawTitle = rawTitle;
|
|
||||||
shows.Add(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ShowSession FindOrCreateShowSession(List<ShowSession> sessions, string sessionNumber)
|
|
||||||
{
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
if (session.session == sessionNumber)
|
|
||||||
{
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var result = new ShowSession();
|
|
||||||
result.session = sessionNumber;
|
|
||||||
sessions.Add(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string LCP(string str1, string str2)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
int minLength = Math.Min(str1.Length, str2.Length);
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
while (i < minLength && str1[i] == str2[i])
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str1.Substring(0, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string CalcBasePathOfSessionExtras(ShowSession session)
|
|
||||||
{
|
|
||||||
if(session.extras.Count == 0) return string.Empty;
|
|
||||||
var result = session.extras[0].path;
|
|
||||||
foreach (var extra in session.extras)
|
|
||||||
{
|
|
||||||
result = LCP(result, extra.path);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendEpisodeFromFile(string path)
|
|
||||||
{
|
|
||||||
var resultsJson = File.ReadAllText(path);
|
|
||||||
var results = JsonConvert.DeserializeObject<List<EpisodeInfo>>(resultsJson);
|
|
||||||
foreach (var episode in results)
|
|
||||||
{
|
|
||||||
AppendEpisode(episode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendEpisode(EpisodeInfo episode)
|
|
||||||
{
|
|
||||||
var show = FindOrCreateShow(_shows, episode.name);
|
|
||||||
var session = FindOrCreateShowSession(show.sessions, episode.session);
|
|
||||||
if (episode.type == "others")
|
|
||||||
{
|
|
||||||
session.extras.Add(episode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
session.episodes.Add(episode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task QueryTMDB(Dictionary<string, string> mapping)
|
|
||||||
{
|
|
||||||
int current = 0;
|
|
||||||
foreach (var show in _shows)
|
|
||||||
{
|
|
||||||
current++;
|
|
||||||
Console.WriteLine($"{current}/{_shows.Count}");
|
|
||||||
var title = show.rawTitle;
|
|
||||||
if(mapping.TryGetValue(title, out var value)) title = value;
|
|
||||||
var result = await _client.SearchTvShowAsync(title, language:"zh-CN");
|
|
||||||
if (result == null || result.Results.Count == 0) continue;
|
|
||||||
var tv = result.Results[0];
|
|
||||||
show.title = tv.Name;
|
|
||||||
show.year = tv.FirstAirDate.Value.Year.ToString();
|
|
||||||
show.tmdbId = tv.Id.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dump(string path)
|
|
||||||
{
|
|
||||||
var result = JsonConvert.SerializeObject(_shows, Formatting.Indented);
|
|
||||||
File.WriteAllText(path, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Load(string path)
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(path);
|
|
||||||
_shows = JsonConvert.DeserializeObject<List<Show>>(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string AddZero(string s)
|
|
||||||
{
|
|
||||||
if(s.Length == 1) return $"0{s}";
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string AddNumberToFileName(string path, int n)
|
|
||||||
{
|
|
||||||
return Path.Combine(Path.GetDirectoryName(path)??"",
|
|
||||||
Path.GetFileNameWithoutExtension(path) + $"({n})" + Path.GetExtension(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MoveFiles(string basePath, string targetBasePath)
|
|
||||||
{
|
|
||||||
HashSet<string> files = new HashSet<string>();
|
|
||||||
Queue<(string, string)> moves = new Queue<(string, string)>();
|
|
||||||
|
|
||||||
foreach (var show in _shows)
|
|
||||||
{
|
|
||||||
foreach (var session in show.sessions)
|
|
||||||
{
|
|
||||||
foreach (var episode in session.episodes)
|
|
||||||
{
|
|
||||||
var oldPath = Path.Combine(basePath, episode.path);
|
|
||||||
var newSubPath = $"{show.title} ({show.year})/Season {session.session}/{show.title} ({show.year}) S{AddZero(episode.session)}E{AddZero(episode.episode)} [{episode.group}]";
|
|
||||||
if (episode.type == "subtitle" && !string.IsNullOrEmpty(episode.language))
|
|
||||||
{
|
|
||||||
newSubPath += $".{episode.language}";
|
|
||||||
}
|
|
||||||
newSubPath += Path.GetExtension(episode.path);
|
|
||||||
var newPath = Path.Combine(targetBasePath, newSubPath);
|
|
||||||
|
|
||||||
var testPath = newPath;
|
|
||||||
int n = 0;
|
|
||||||
while (files.Contains(testPath))
|
|
||||||
{
|
|
||||||
testPath = AddNumberToFileName(newPath, ++n);
|
|
||||||
}
|
|
||||||
newPath = testPath;
|
|
||||||
|
|
||||||
files.Add(newPath);
|
|
||||||
moves.Enqueue((oldPath, newPath));
|
|
||||||
Console.WriteLine($"{oldPath} -> {newPath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var extraPath = CalcBasePathOfSessionExtras(session);
|
|
||||||
foreach (var episode in session.extras)
|
|
||||||
{
|
|
||||||
var oldPath = Path.Combine(basePath, episode.path);
|
|
||||||
var newSubPath = $"{show.title} ({show.year})/Season {session.session}/extras";
|
|
||||||
var subPath = episode.path.Substring(extraPath.Length);
|
|
||||||
var newPath = Path.Combine(targetBasePath, newSubPath, subPath);
|
|
||||||
|
|
||||||
var testPath = newPath;
|
|
||||||
int n = 0;
|
|
||||||
while (files.Contains(testPath))
|
|
||||||
{
|
|
||||||
testPath = AddNumberToFileName(newPath, ++n);
|
|
||||||
}
|
|
||||||
newPath = testPath;
|
|
||||||
|
|
||||||
files.Add(newPath);
|
|
||||||
moves.Enqueue((oldPath, newPath));
|
|
||||||
Console.WriteLine($"{oldPath} -> {newPath}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (moves.Count > 0)
|
|
||||||
{
|
|
||||||
var move = moves.Dequeue();
|
|
||||||
if (!File.Exists(move.Item1)) continue;
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(move.Item2));
|
|
||||||
File.Move(move.Item1, move.Item2);
|
|
||||||
Console.WriteLine($"{move.Item1} -> {move.Item2}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
Src/ConfigSchema/ProxyConfig.cs
Normal file
9
Src/ConfigSchema/ProxyConfig.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using LLCSharpUtils;
|
||||||
|
|
||||||
|
namespace BangumiRenamer.ConfigSchema;
|
||||||
|
|
||||||
|
[ConfigItem("Proxy")]
|
||||||
|
public class ProxyConfig : IConfigItem
|
||||||
|
{
|
||||||
|
public string HttpProxy = "";
|
||||||
|
}
|
||||||
9
Src/ConfigSchema/TMDBConfig.cs
Normal file
9
Src/ConfigSchema/TMDBConfig.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using LLCSharpUtils;
|
||||||
|
|
||||||
|
namespace BangumiRenamer.Config;
|
||||||
|
|
||||||
|
[ConfigItem("TMDB")]
|
||||||
|
public class TMDBConfig : IConfigItem
|
||||||
|
{
|
||||||
|
public string ApiKey = "";
|
||||||
|
}
|
||||||
13
Src/Entity/Show.cs
Normal file
13
Src/Entity/Show.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace BangumiRenamer.Data;
|
||||||
|
|
||||||
|
public class Show
|
||||||
|
{
|
||||||
|
public string title;
|
||||||
|
public List<Session> sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
public int id;
|
||||||
|
public List<int> episodes;
|
||||||
|
}
|
||||||
7
Src/Program.cs
Normal file
7
Src/Program.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Src/Tools/Config.cs
Normal file
15
Src/Tools/Config.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace BangumiRenamer.Tools;
|
||||||
|
|
||||||
|
public class Config
|
||||||
|
{
|
||||||
|
private static readonly Lazy<LLCSharpUtils.Config> _lazyConfig = new (() =>
|
||||||
|
{
|
||||||
|
var config = new LLCSharpUtils.Config
|
||||||
|
{
|
||||||
|
ConfigPath = "config.json"
|
||||||
|
};
|
||||||
|
if (!config.Load(true)) throw new Exception("Failed to load config.json");
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
public static LLCSharpUtils.Config G => _lazyConfig.Value;
|
||||||
|
}
|
||||||
16
Src/Tools/EmbededResourceViewer.cs
Normal file
16
Src/Tools/EmbededResourceViewer.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using LLCSharpUtils;
|
||||||
|
|
||||||
|
namespace BangumiRenamer.Tools;
|
||||||
|
|
||||||
|
public class EmbededResourceViewer
|
||||||
|
{
|
||||||
|
public static void PrintResourceNames()
|
||||||
|
{
|
||||||
|
var loader = new EmbeddedResourceLoader();
|
||||||
|
var names = loader.GetAllResNames();
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
Logger.G.Info(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Src/Tools/FolderCloner.cs
Normal file
71
Src/Tools/FolderCloner.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using NativeFileDialogSharp;
|
||||||
|
|
||||||
|
namespace BangumiRenamer.Tools;
|
||||||
|
|
||||||
|
public static class FolderCloner
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var result = Dialog.FolderPicker();
|
||||||
|
if (!result.IsOk) return;
|
||||||
|
var source = result.Path;
|
||||||
|
|
||||||
|
result = Dialog.FolderPicker();
|
||||||
|
if (!result.IsOk) return;
|
||||||
|
var dest = Path.Combine(result.Path, Path.GetFileNameWithoutExtension(source.Replace("\\\\", "")));
|
||||||
|
|
||||||
|
var finalDest = dest;
|
||||||
|
int suffix = 1;
|
||||||
|
while (Directory.Exists(finalDest))
|
||||||
|
{
|
||||||
|
finalDest = dest + suffix;
|
||||||
|
++suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloneStructureWithEmptyFiles(source, finalDest);
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = finalDest,
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "open"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CloneStructureWithEmptyFiles(string source, string dest)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(source))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Directory.Exists(dest))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var queue = new Queue<string>();
|
||||||
|
queue.Enqueue(source);
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var dir = queue.Dequeue();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var filePath in Directory.GetFiles(dir))
|
||||||
|
{
|
||||||
|
var newFilePath = Path.Combine(dest, Path.GetRelativePath(source, filePath));
|
||||||
|
using var _ = File.Create(newFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dirPath in Directory.GetDirectories(dir))
|
||||||
|
{
|
||||||
|
queue.Enqueue(dirPath);
|
||||||
|
var newDirPath = Path.Combine(dest, Path.GetRelativePath(source, dirPath));
|
||||||
|
Directory.CreateDirectory(newDirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.G.Error(ex, "Clone");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Src/Tools/Logger.cs
Normal file
15
Src/Tools/Logger.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using LLCSharpUtils;
|
||||||
|
|
||||||
|
namespace BangumiRenamer.Tools;
|
||||||
|
|
||||||
|
public class Logger
|
||||||
|
{
|
||||||
|
private static readonly Lazy<Log> _lazy = new (() =>
|
||||||
|
{
|
||||||
|
var log = new Log(new LogConfig());
|
||||||
|
if (!log.Init()) throw new Exception("Failed to initialize log");
|
||||||
|
return log;
|
||||||
|
});
|
||||||
|
|
||||||
|
public static Log G => _lazy.Value;
|
||||||
|
}
|
||||||
96
Src/Tools/ShowCompletionChecker.cs
Normal file
96
Src/Tools/ShowCompletionChecker.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
using BangumiRenamer.Config;
|
||||||
|
using BangumiRenamer.ConfigSchema;
|
||||||
|
using TMDbLib.Client;
|
||||||
|
using NativeFileDialogSharp;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BangumiRenamer.Data;
|
||||||
|
|
||||||
|
namespace BangumiRenamer.Tools;
|
||||||
|
|
||||||
|
public static class ShowCompletionChecker
|
||||||
|
{
|
||||||
|
static List<Show> FindShows(string checkPath)
|
||||||
|
{
|
||||||
|
var shows = new List<Show>();
|
||||||
|
var showPaths = Directory.GetDirectories(checkPath);
|
||||||
|
foreach (var showPath in showPaths)
|
||||||
|
{
|
||||||
|
var sessions = new List<Session>();
|
||||||
|
var sessionPaths = Directory.GetDirectories(showPath);
|
||||||
|
foreach (var sessionPath in sessionPaths)
|
||||||
|
{
|
||||||
|
var episodePaths = Directory.GetFiles(sessionPath);
|
||||||
|
HashSet<int> episodes = new HashSet<int>();
|
||||||
|
foreach (var episodePath in episodePaths)
|
||||||
|
{
|
||||||
|
var matches = Regex.Matches(episodePath, @".*S\d+E(\d+).*");
|
||||||
|
if (matches.Count == 0) continue;
|
||||||
|
var episode = int.Parse(matches[0].Groups[1].Value);
|
||||||
|
episodes.Add(episode);
|
||||||
|
}
|
||||||
|
var session = new Session
|
||||||
|
{
|
||||||
|
id = int.Parse(Path.GetFileName(sessionPath).Replace("Season ", "")),
|
||||||
|
episodes = episodes.ToList()
|
||||||
|
};
|
||||||
|
sessions.Add(session);
|
||||||
|
}
|
||||||
|
shows.Add(new Show
|
||||||
|
{
|
||||||
|
title = Path.GetFileName(showPath),
|
||||||
|
sessions = sessions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return shows;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var dir = Dialog.FolderPicker();
|
||||||
|
if (!dir.IsOk) return;
|
||||||
|
var checkPath = dir.Path;
|
||||||
|
|
||||||
|
var shows = FindShows(checkPath);
|
||||||
|
Logger.G.Info($"Total Shows: {shows.Count}");
|
||||||
|
|
||||||
|
var client = new TMDbClient(
|
||||||
|
apiKey: Config.G.Get<TMDBConfig>().ApiKey ,
|
||||||
|
proxy: new WebProxy(Config.G.Get<ProxyConfig>().HttpProxy));
|
||||||
|
|
||||||
|
var output = new StringBuilder();
|
||||||
|
foreach (var t in shows)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(t.title, @"(.*) \((\d{4})\)");
|
||||||
|
var title = match.Groups[1].Value;
|
||||||
|
var year = int.Parse(match.Groups[2].Value);
|
||||||
|
var result = client.SearchTvShowAsync(title, firstAirDateYear: year).Result;
|
||||||
|
var info = result.Results.FirstOrDefault();
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
Logger.G.Error($"找不到对应的TV:{t.title}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (var session in t.sessions)
|
||||||
|
{
|
||||||
|
var sessionInfo = client.GetTvSeasonAsync(info.Id, session.id).Result;
|
||||||
|
if (sessionInfo == null)
|
||||||
|
{
|
||||||
|
Logger.G.Error($"季度对不上,可能找错了:{t.title} -> {info.OriginalName}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
foreach (var episode in sessionInfo.Episodes)
|
||||||
|
{
|
||||||
|
if (DateTime.Now.AddDays(-7) < episode.AirDate) continue;
|
||||||
|
if (!session.episodes.Contains(episode.EpisodeNumber))
|
||||||
|
{
|
||||||
|
output.AppendLine($"{title} 的第 {session.id} 季少了第 {episode.EpisodeNumber} 集");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.G.Info(output.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +0,0 @@
|
|||||||
namespace ConsoleApp1;
|
|
||||||
|
|
||||||
public class TMDBHelper
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"path": "The.Name.of.the.People.2017.EP01-55.HD1080P.X264.AAC.Mandarin.CHS.Mp4Ba/The.Name.of.the.People.2017.EP01.HD1080P.X264.AAC.Mandarin.CHS.Mp4Ba.mp4",
|
|
||||||
"name": "The Name of the People",
|
|
||||||
"session": "1",
|
|
||||||
"episode": "1",
|
|
||||||
"group": "Mp4Ba",
|
|
||||||
"type": "episode",
|
|
||||||
"language": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "The.Name.of.the.People.2017.EP01-55.HD1080P.X264.AAC.Mandarin.CHS.Mp4Ba/The.Name.of.the.People.2017.EP02.HD1080P.X264.AAC.Mandarin.CHS.Mp4Ba.mp4",
|
|
||||||
"name": "The Name of the People",
|
|
||||||
"session": "1",
|
|
||||||
"episode": "2",
|
|
||||||
"group": "Mp4Ba",
|
|
||||||
"type": "episode",
|
|
||||||
"language": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "The.Name.of.the.People.2017.EP01-55.HD1080P.X264.AAC.Mandarin.CHS.Mp4Ba/The.Name.of.the.People.2017.EP03.HD1080P.X264.AAC.Mandarin.CHS.Mp4Ba.mp4",
|
|
||||||
"name": "The Name of the People",
|
|
||||||
"session": "1",
|
|
||||||
"episode": "3",
|
|
||||||
"group": "Mp4Ba",
|
|
||||||
"type": "episode",
|
|
||||||
"language": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Loading…
x
Reference in New Issue
Block a user