Skip to content

Commit 5a6f5b0

Browse files
committed
feat(subtitle): enhance subtitle local search [Fixes #93]
1 parent 3aac04b commit 5a6f5b0

File tree

3 files changed

+221
-28
lines changed

3 files changed

+221
-28
lines changed

FlyleafLib/Engine/Config.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using System.Text.Json.Serialization;
66
using System.Threading;
77
using System.Threading.Tasks;
8-
8+
using FlyleafLib.Controls.WPF;
99
using FlyleafLib.MediaFramework.MediaDecoder;
1010
using FlyleafLib.MediaFramework.MediaFrame;
1111
using FlyleafLib.MediaFramework.MediaRenderer;
@@ -1122,6 +1122,26 @@ public bool LanguageFallbackSecondarySame
11221122
public bool SearchLocal { get => _SearchLocal; set => Set(ref _SearchLocal, value); }
11231123
bool _SearchLocal = false;
11241124

1125+
public const string DefaultSearchLocalPaths = "subs; subtitles";
1126+
1127+
public string SearchLocalPaths
1128+
{
1129+
get;
1130+
set
1131+
{
1132+
if (Set(ref field, value))
1133+
{
1134+
CmdResetSearchLocalPaths.OnCanExecuteChanged();
1135+
}
1136+
}
1137+
} = DefaultSearchLocalPaths;
1138+
1139+
[JsonIgnore]
1140+
public RelayCommand CmdResetSearchLocalPaths => field ??= new((_) =>
1141+
{
1142+
SearchLocalPaths = DefaultSearchLocalPaths;
1143+
}, (_) => SearchLocalPaths != DefaultSearchLocalPaths);
1144+
11251145
/// <summary>
11261146
/// Allowed input types to be searched locally for subtitles (empty list allows all types)
11271147
/// </summary>

FlyleafLib/Plugins/OpenSubtitles.cs

Lines changed: 138 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
using FlyleafLib.MediaFramework.MediaStream;
2-
using Lingua;
1+
using System.Collections.Generic;
32
using System.IO;
3+
using System.Linq;
44
using System.Text;
5+
using FlyleafLib.MediaFramework.MediaStream;
6+
using Lingua;
57

68
namespace FlyleafLib.Plugins;
79

810
public class OpenSubtitles : PluginBase, IOpenSubtitles, ISearchLocalSubtitles
911
{
1012
public new int Priority { get; set; } = 3000;
1113

12-
private readonly Lazy<LanguageDetector> _languageDetector = new(() =>
14+
private static readonly Lazy<LanguageDetector> LanguageDetector = new(() =>
1315
{
1416
LanguageDetector detector = LanguageDetectorBuilder
1517
.FromAllLanguages()
@@ -18,9 +20,11 @@ public class OpenSubtitles : PluginBase, IOpenSubtitles, ISearchLocalSubtitles
1820
return detector;
1921
}, true);
2022

23+
private static readonly HashSet<string> ExtSet = new(Utils.ExtensionsSubtitles, StringComparer.OrdinalIgnoreCase);
24+
2125
public OpenSubtitlesResults Open(string url)
2226
{
23-
foreach(var extStream in Selected.ExternalSubtitlesStreamsAll)
27+
foreach (var extStream in Selected.ExternalSubtitlesStreamsAll)
2428
if (extStream.Url == url)
2529
return new OpenSubtitlesResults(extStream);
2630

@@ -58,32 +62,53 @@ public void SearchLocalSubtitles()
5862
{
5963
try
6064
{
61-
// Checks for text subtitles with the same file name and reads them
62-
// TODO: L: Search for subtitles with filenames like video file.XXX.srt
63-
// TODO: L: Allow reading from specific folders as well.
64-
foreach (string ext in Utils.ExtensionsSubtitles)
65+
string mediaDir = Path.GetDirectoryName(Playlist.Url);
66+
string mediaName = Path.GetFileNameWithoutExtension(Playlist.Url);
67+
68+
OrderedDictionary<string, Language> result = new(StringComparer.OrdinalIgnoreCase);
69+
70+
CollectFromDirectory(mediaDir, mediaName, result);
71+
72+
// also search in subdirectories
73+
string paths = Config.Subtitles.SearchLocalPaths;
74+
if (!string.IsNullOrWhiteSpace(paths))
6575
{
66-
string subPath = Path.ChangeExtension(Playlist.Url, ext);
67-
if (File.Exists(subPath))
76+
foreach (Range seg in paths.AsSpan().Split(';'))
6877
{
69-
ExternalSubtitlesStream sub = new()
70-
{
71-
Url = subPath,
72-
Title = Path.GetFileNameWithoutExtension(subPath),
73-
Downloaded = true,
74-
IsBitmap = IsSubtitleBitmap(subPath),
75-
};
78+
var path = paths.AsSpan(seg).Trim();
79+
if (path.IsEmpty) continue;
7680

77-
if (Config.Subtitles.LanguageAutoDetect && !sub.IsBitmap)
81+
string searchDir = !Path.IsPathRooted(path)
82+
? Path.Join(mediaDir, path)
83+
: path.ToString();
84+
85+
if (Directory.Exists(searchDir))
7886
{
79-
sub.Language = DetectLanguage(subPath);
80-
sub.LanguageDetected = true;
87+
CollectFromDirectory(searchDir, mediaName, result);
8188
}
89+
}
90+
}
8291

83-
Log.Debug($"Adding [{sub.Language.TopEnglishName}] {subPath}");
92+
foreach (var (path, lang) in result)
93+
{
94+
ExternalSubtitlesStream sub = new()
95+
{
96+
Url = path,
97+
Title = Path.GetFileNameWithoutExtension(path),
98+
Downloaded = true,
99+
IsBitmap = IsSubtitleBitmap(path),
100+
Language = lang
101+
};
84102

85-
AddExternalStream(sub);
103+
if (Config.Subtitles.LanguageAutoDetect && !sub.IsBitmap && lang == Language.Unknown)
104+
{
105+
sub.Language = DetectLanguage(path);
106+
sub.LanguageDetected = true;
86107
}
108+
109+
Log.Debug($"Adding [{sub.Language.TopEnglishName}] {path}");
110+
111+
AddExternalStream(sub);
87112
}
88113
}
89114
catch (Exception e)
@@ -92,8 +117,96 @@ public void SearchLocalSubtitles()
92117
}
93118
}
94119

120+
private static void CollectFromDirectory(string searchDir, string filename, IDictionary<string, Language> result)
121+
{
122+
HashSet<int> added = null;
123+
124+
// Get files starting with the same filename
125+
List<string> fileList;
126+
try
127+
{
128+
fileList = Directory.GetFiles(searchDir, $"{filename}.*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive })
129+
.ToList();
130+
}
131+
catch
132+
{
133+
return;
134+
}
135+
136+
if (fileList.Count == 0)
137+
{
138+
return;
139+
}
140+
141+
var files = fileList.Select(f => new
142+
{
143+
FullPath = f,
144+
FileName = Path.GetFileName(f)
145+
}).ToList();
146+
147+
// full match with top priority (video.srt, video.ass)
148+
foreach (string ext in ExtSet)
149+
{
150+
string expect = $"{filename}.{ext}";
151+
int match = files.FindIndex(x => string.Equals(x.FileName, expect, StringComparison.OrdinalIgnoreCase));
152+
if (match != -1)
153+
{
154+
result.TryAdd(files[match].FullPath, Language.Unknown);
155+
added ??= new HashSet<int>();
156+
added.Add(match);
157+
}
158+
}
159+
160+
// head match (video.*.srt, video.*.ass)
161+
var extSetLookup = ExtSet.GetAlternateLookup<ReadOnlySpan<char>>();
162+
foreach (var (i, x) in files.Index())
163+
{
164+
// skip full match
165+
if (added != null && added.Contains(i))
166+
{
167+
continue;
168+
}
169+
170+
var span = x.FileName.AsSpan();
171+
var fileExt = Path.GetExtension(span).TrimStart('.');
172+
173+
// Check if the file is a subtitle file by its extension
174+
if (extSetLookup.Contains(fileExt))
175+
{
176+
var name = Path.GetFileNameWithoutExtension(span);
177+
178+
if (!name.StartsWith(filename + '.', StringComparison.OrdinalIgnoreCase))
179+
{
180+
continue;
181+
}
182+
183+
Language lang = Language.Unknown;
184+
185+
var extraPart = name.Slice(filename.Length + 1); // Skip file name and dot
186+
if (extraPart.Length > 0)
187+
{
188+
foreach (var codeSeg in extraPart.Split('.'))
189+
{
190+
var code = extraPart[codeSeg];
191+
if (code.Length > 0)
192+
{
193+
Language parsed = Language.Get(code.ToString());
194+
if (!string.IsNullOrEmpty(parsed.IdSubLanguage) && parsed.IdSubLanguage != "und")
195+
{
196+
lang = parsed;
197+
break;
198+
}
199+
}
200+
}
201+
}
202+
203+
result.TryAdd(x.FullPath, lang);
204+
}
205+
}
206+
}
207+
95208
// TODO: L: To check the contents of a file by determining the bitmap.
96-
private bool IsSubtitleBitmap(string path)
209+
private static bool IsSubtitleBitmap(string path)
97210
{
98211
try
99212
{
@@ -108,7 +221,7 @@ private bool IsSubtitleBitmap(string path)
108221
}
109222

110223
// TODO: L: Would it be better to check with SubtitlesManager for network subtitles?
111-
private Language DetectLanguage(string path)
224+
private static Language DetectLanguage(string path)
112225
{
113226
if (!File.Exists(path))
114227
{
@@ -139,7 +252,7 @@ private Language DetectLanguage(string path)
139252

140253
string content = encoding.GetString(data);
141254

142-
var detectedLanguage = _languageDetector.Value.DetectLanguageOf(content);
255+
var detectedLanguage = LanguageDetector.Value.DetectLanguageOf(content);
143256

144257
if (detectedLanguage == Lingua.Language.Unknown)
145258
{

LLPlayer/Controls/Settings/SettingsSubtitles.xaml

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,72 @@
3434

3535
<StackPanel Orientation="Horizontal">
3636
<TextBlock
37-
Width="180"
38-
Text="Search Local" />
37+
Width="180">
38+
Search Local
39+
<InlineUIContainer BaselineAlignment="Center" Cursor="Help">
40+
<ToolTipService.ToolTip>
41+
<TextBlock
42+
TextWrapping="Wrap"
43+
MaxWidth="400">
44+
<Run Text="If enabled, subtitle files in the same path as the media file are automatically opened. The following subtitle files will be opened."/>
45+
<LineBreak />
46+
<LineBreak /><Run Text="- video.srt" />
47+
<LineBreak /><Run Text="- video.en.srt (language is set from the lang code)" />
48+
<LineBreak /><Run Text="- video.eng.srt (same as above)" />
49+
<LineBreak /><Run Text="- video.foo.srt (language is auto-detected)" />
50+
<LineBreak /><Run Text="- path\video.srt" />
51+
</TextBlock>
52+
</ToolTipService.ToolTip>
53+
<materialDesign:PackIcon
54+
Kind="Information"
55+
Width="16" Height="16"
56+
Margin="4 0 0 0" />
57+
</InlineUIContainer>
58+
</TextBlock>
3959
<ToggleButton
4060
IsChecked="{Binding FL.PlayerConfig.Subtitles.SearchLocal}" />
4161
</StackPanel>
4262

63+
<DockPanel Visibility="{Binding FL.PlayerConfig.Subtitles.SearchLocal, Converter={StaticResource BooleanToVisibilityConv}}">
64+
<TextBlock
65+
Width="180">
66+
<Run Text=" Search Local Paths" />
67+
<InlineUIContainer BaselineAlignment="Center" Cursor="Help">
68+
<ToolTipService.ToolTip>
69+
<TextBlock
70+
Text="Specify additional folder paths to open subtitles, separated by semicolons. If left blank, subtitles will be opened only from the current directory. Folder case is ignored. Both relative and absolute paths can be specified."
71+
TextWrapping="Wrap"
72+
MaxWidth="400" />
73+
</ToolTipService.ToolTip>
74+
<materialDesign:PackIcon
75+
Kind="Information"
76+
Width="16" Height="16"
77+
Margin="4 0 0 0" />
78+
</InlineUIContainer>
79+
</TextBlock>
80+
<Grid>
81+
<Grid.ColumnDefinitions>
82+
<ColumnDefinition Width="*" />
83+
<ColumnDefinition Width="Auto" />
84+
</Grid.ColumnDefinitions>
85+
<TextBox
86+
Grid.Column="0"
87+
HorizontalContentAlignment="Left"
88+
Text="{Binding FL.PlayerConfig.Subtitles.SearchLocalPaths}" />
89+
<Button
90+
Grid.Column="1"
91+
Margin="10 0 0 0"
92+
ToolTip="Reset to default"
93+
Style="{StaticResource MaterialDesignIconButton}"
94+
HorizontalAlignment="Center"
95+
Width="24"
96+
Height="24"
97+
Command="{Binding FL.PlayerConfig.Subtitles.CmdResetSearchLocalPaths}">
98+
<materialDesign:PackIcon Kind="BackupRestore" />
99+
</Button>
100+
</Grid>
101+
</DockPanel>
102+
43103
<StackPanel Orientation="Horizontal">
44104
<TextBlock
45105
Width="180">

0 commit comments

Comments
 (0)