实现RawParser
This commit is contained in:
parent
cf128159d1
commit
9cf948ec9c
@ -13,14 +13,19 @@ namespace Learn.Component;
|
||||
|
||||
public partial class MainTreePanel : Tree
|
||||
{
|
||||
public class ColumnView
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Width { get; set; }
|
||||
}
|
||||
|
||||
public class MainTreeData
|
||||
{
|
||||
public List<string> Columns { get; set; }
|
||||
public List<ColumnView> Columns { get; set; }
|
||||
public Dictionary<string, Item> Items { get; set; }
|
||||
}
|
||||
|
||||
|
||||
private readonly List<string> _columns = new();
|
||||
private readonly List<ColumnView> _columns = new();
|
||||
private readonly HashSet<TreeNode> _selectingNodes = new();
|
||||
private readonly Dictionary<TreeItem, TreeNode> _mapper = new();
|
||||
private readonly Dictionary<string, Item> _items = new();
|
||||
@ -41,22 +46,28 @@ public partial class MainTreePanel : Tree
|
||||
UpdateColumns();
|
||||
}
|
||||
|
||||
public void AddColumn(string columnName, int index)
|
||||
public void AddColumn(string columnName, int index, int width)
|
||||
{
|
||||
if (string.IsNullOrEmpty(columnName)) return;
|
||||
if (_columns.Contains(columnName)) return;
|
||||
if (index < 0)
|
||||
var targets = _columns.Where(column => column.Name == columnName).ToList();
|
||||
|
||||
if (index < 0) index = _columns.Count;
|
||||
if (width < 0) width = 400;
|
||||
if (targets.Any())
|
||||
{
|
||||
_columns.Add(columnName);
|
||||
targets.First().Width = width;
|
||||
}
|
||||
else
|
||||
{
|
||||
_columns.Insert(index, new ColumnView { Name = columnName, Width = width });
|
||||
}
|
||||
_columns.Insert(index, columnName);
|
||||
UpdateColumns();
|
||||
}
|
||||
|
||||
public void RemoveColumn(string columnName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(columnName)) return;
|
||||
_columns.Remove(columnName);
|
||||
_columns.RemoveAll(column => column.Name == columnName);
|
||||
UpdateColumns();
|
||||
}
|
||||
|
||||
@ -72,8 +83,8 @@ public partial class MainTreePanel : Tree
|
||||
int index = 1;
|
||||
foreach (var column in _columns)
|
||||
{
|
||||
SetColumnTitle(index, column);
|
||||
SetColumnCustomMinimumWidth(index, 400);
|
||||
SetColumnTitle(index, column.Name);
|
||||
SetColumnCustomMinimumWidth(index, column.Width);
|
||||
SetColumnExpand(index, true);
|
||||
index += 1;
|
||||
}
|
||||
@ -83,14 +94,12 @@ public partial class MainTreePanel : Tree
|
||||
index = 1;
|
||||
foreach (var column in _columns)
|
||||
{
|
||||
if (kv.Value.TryGetValue(column, out var value, out _))
|
||||
if (!kv.Value.TryGetValue(column.Name, out var value, out _))
|
||||
{
|
||||
kv.Key.SetText(index, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
kv.Key.SetText(index, "");
|
||||
value = "";
|
||||
}
|
||||
kv.Key.SetText(index, value);
|
||||
kv.Key.SetTextAlignment(index, HorizontalAlignment.Center);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
9
Config/AppConfig.cs
Normal file
9
Config/AppConfig.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Learn.Utils;
|
||||
|
||||
namespace Learn.Config;
|
||||
|
||||
[ConfigItem("APP")]
|
||||
public class AppConfig : IConfigItem
|
||||
{
|
||||
public string ScanPath = "";
|
||||
}
|
||||
71
Main.cs
71
Main.cs
@ -6,6 +6,7 @@ using Godot;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Learn.Component;
|
||||
using Learn.Config;
|
||||
using Learn.Parsers;
|
||||
using Learn.Utils;
|
||||
|
||||
@ -32,6 +33,7 @@ public partial class Main : Node
|
||||
[ExportGroup("主展示面板")]
|
||||
[Export] private MainTreePanel _mainTreePanel;
|
||||
[Export] private LineEdit _columnIndexText;
|
||||
[Export] private LineEdit _columnWidthText;
|
||||
[Export] private LineEdit _columnText;
|
||||
[Export] private Button _addColumnButton;
|
||||
[Export] private Button _removeColumnButton;
|
||||
@ -50,7 +52,12 @@ public partial class Main : Node
|
||||
_removeKeyButton.Pressed += _nodeInfoEditPanel.RemoveKeyValue;
|
||||
|
||||
_mainTreePanel.MultiSelected += (_, _, _) => { _refreshPanels = true; };
|
||||
_addColumnButton.Pressed += () => _mainTreePanel.AddColumn(_columnText.Text, int.TryParse(_columnIndexText.Text, out var index) ? index : -1);
|
||||
_addColumnButton.Pressed += () =>
|
||||
{
|
||||
_mainTreePanel.AddColumn(_columnText.Text,
|
||||
int.TryParse(_columnIndexText.Text, out var index) ? index : -1,
|
||||
int.TryParse(_columnWidthText.Text, out var width) ? width : -1);
|
||||
};
|
||||
_removeColumnButton.Pressed += () => _mainTreePanel.RemoveColumn(_columnText.Text);
|
||||
_clearColumnButton.Pressed += () => _mainTreePanel.ClearColumns();
|
||||
|
||||
@ -64,17 +71,15 @@ public partial class Main : Node
|
||||
_resetButton.Pressed += DoReset;
|
||||
_loadButton.Pressed += LoadData;
|
||||
_doParseButton.Text = "开始解析";
|
||||
|
||||
LoadData();
|
||||
|
||||
_mainTreePanel.Scan("C:/Users/15401/Desktop/2025-up7"); // todo: 弄好后删掉
|
||||
|
||||
FoldAll();
|
||||
_refreshPanels = true;
|
||||
|
||||
_configs = new Configs();
|
||||
_configs.ConfigPath = "config.json";
|
||||
_configs.Load(true);
|
||||
|
||||
LoadData();
|
||||
ScanDir();
|
||||
FoldAll();
|
||||
_refreshPanels = true;
|
||||
}
|
||||
|
||||
private void LoadData()
|
||||
@ -101,17 +106,9 @@ public partial class Main : Node
|
||||
{
|
||||
var root = _mainTreePanel.GetRoot();
|
||||
if (root == null) return;
|
||||
var queue = new Queue<TreeItem>();
|
||||
queue.Enqueue(root);
|
||||
while (queue.Count > 0)
|
||||
foreach (var child in root.GetChildren())
|
||||
{
|
||||
var treeItem = queue.Dequeue();
|
||||
foreach (var child in treeItem.GetChildren())
|
||||
{
|
||||
if(child == null) continue;
|
||||
child.Collapsed = collapsed;
|
||||
queue.Enqueue(child);
|
||||
}
|
||||
child.SetCollapsedRecursive(collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,7 +127,8 @@ public partial class Main : Node
|
||||
|
||||
private async void ScanDir()
|
||||
{
|
||||
var path = await _dirSelector.SelectFolderAsync();
|
||||
// var path = await _dirSelector.SelectFolderAsync();
|
||||
var path = _configs.Get<AppConfig>().ScanPath;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
@ -148,40 +146,11 @@ public partial class Main : Node
|
||||
var originName = _doParseButton.Text;
|
||||
_doParseButton.Disabled = true;
|
||||
|
||||
ItemParser parser = new TMDBParser(_configs);
|
||||
ItemParser parser = new RawParser();
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var stack = new Stack<TreeItem>();
|
||||
stack.Push(root);
|
||||
|
||||
while (stack.Count > 0)
|
||||
if (_mainTreePanel.Query(root, out var node))
|
||||
{
|
||||
_doParseButton.Text = "解析中";
|
||||
if (stopwatch.ElapsedMilliseconds > 2000)
|
||||
{
|
||||
_mainTreePanel.UpdateColumns();
|
||||
stopwatch.Restart();
|
||||
}
|
||||
var treeItem = stack.Pop();
|
||||
foreach (var child in treeItem.GetChildren())
|
||||
{
|
||||
if(child == null) continue;
|
||||
if (_mainTreePanel.Query(child, out var node))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await parser.DoParse(node))
|
||||
{
|
||||
GD.PrintErr($"解析{child.GetText(0)}失败");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"解析{child.GetText(0)}出现异常:" + ex);
|
||||
}
|
||||
}
|
||||
stack.Push(child);
|
||||
}
|
||||
await parser.Parse(node);
|
||||
}
|
||||
|
||||
_doParseButton.Text = originName;
|
||||
|
||||
@ -17,7 +17,7 @@ grow_vertical = 2
|
||||
[node name="FileDirDialog" type="FileDialog" parent="."]
|
||||
script = ExtResource("1_d2g23")
|
||||
|
||||
[node name="Main" type="Node" parent="." node_paths=PackedStringArray("_dirSelector", "_openDirButton", "_doParseButton", "_saveButton", "_resetButton", "_loadButton", "_nodeInfoEditPanel", "_addKeyButton", "_removeKeyButton", "_inspectorPanel", "_mainTreePanel", "_columnIndexText", "_columnText", "_addColumnButton", "_removeColumnButton", "_clearColumnButton", "_expandAllButton", "_foldAllButton")]
|
||||
[node name="Main" type="Node" parent="." node_paths=PackedStringArray("_dirSelector", "_openDirButton", "_doParseButton", "_saveButton", "_resetButton", "_loadButton", "_nodeInfoEditPanel", "_addKeyButton", "_removeKeyButton", "_inspectorPanel", "_mainTreePanel", "_columnIndexText", "_columnWidthText", "_columnText", "_addColumnButton", "_removeColumnButton", "_clearColumnButton", "_expandAllButton", "_foldAllButton")]
|
||||
script = ExtResource("2_0727o")
|
||||
_dirSelector = NodePath("../FileDirDialog")
|
||||
_openDirButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/文件夹操作/ScanDir")
|
||||
@ -31,6 +31,7 @@ _removeKeyButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/
|
||||
_inspectorPanel = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer2/InspectorPanel")
|
||||
_mainTreePanel = NodePath("../MarginContainer/HSplitContainer/MainTreePanel")
|
||||
_columnIndexText = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer2/ColumnIndex")
|
||||
_columnWidthText = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer3/ColumnWidth")
|
||||
_columnText = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer/ColunmText")
|
||||
_addColumnButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer/AddColumn")
|
||||
_removeColumnButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer/RemoveColumn")
|
||||
@ -137,6 +138,17 @@ layout_mode = 2
|
||||
layout_mode = 2
|
||||
text = "行"
|
||||
|
||||
[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label2" type="Label" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer3"]
|
||||
layout_mode = 2
|
||||
text = "宽度:"
|
||||
|
||||
[node name="ColumnWidth" type="LineEdit" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作/HBoxContainer3"]
|
||||
custom_minimum_size = Vector2(50, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/树表操作"]
|
||||
layout_mode = 2
|
||||
|
||||
|
||||
@ -16,5 +16,5 @@ namespace Learn.Parsers;
|
||||
/// </summary>
|
||||
public interface ItemParser
|
||||
{
|
||||
public Task<bool> DoParse(TreeNode node);
|
||||
public Task Parse(TreeNode node);
|
||||
}
|
||||
171
Parsers/RawParser.cs
Normal file
171
Parsers/RawParser.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Learn.Models;
|
||||
|
||||
namespace Learn.Parsers;
|
||||
|
||||
public class RawParser : ItemParser
|
||||
{
|
||||
private string ParseSeasonFromCN(string season)
|
||||
{
|
||||
if (int.TryParse(season, out _)) return season;
|
||||
|
||||
switch (season)
|
||||
{
|
||||
case "零": return "0";
|
||||
case "一": return "1";
|
||||
case "二": return "2";
|
||||
case "三": return "3";
|
||||
case "四": return "4";
|
||||
case "五": return "5";
|
||||
case "六": return "6";
|
||||
case "七": return "7";
|
||||
case "八": return "8";
|
||||
case "九": return "9";
|
||||
}
|
||||
return season;
|
||||
}
|
||||
|
||||
|
||||
private (string, string) SplitTitleAndSeason(string rawTitle)
|
||||
{
|
||||
var match1 = Regex.Match(rawTitle, @"第(.+)季");
|
||||
if (match1.Success)
|
||||
{
|
||||
var seasonStr = match1.Groups[0].Value.Trim();
|
||||
var season = ParseSeasonFromCN(match1.Groups[1].Value.Trim());
|
||||
var title = rawTitle.Replace(seasonStr, "").Trim();
|
||||
return (title, season);
|
||||
}
|
||||
|
||||
var match2 = Regex.Match(rawTitle, @"[Ss]eason *(\d+)");
|
||||
if (match2.Success)
|
||||
{
|
||||
var seasonStr = match1.Groups[0].Value.Trim();
|
||||
var season = int.Parse(match1.Groups[1].Value.Trim()).ToString();
|
||||
var title = rawTitle.Replace(seasonStr, "").Trim();
|
||||
return (title, season);
|
||||
}
|
||||
|
||||
return (rawTitle, null);
|
||||
}
|
||||
|
||||
private bool TryParseRawTitle(Item item, out string rawTitle)
|
||||
{
|
||||
rawTitle = null;
|
||||
var name = item.Name();
|
||||
var matches = Regex.Matches(name, @"[^\[\]_]+").Select(match => match.Value).ToList();
|
||||
matches.RemoveAll(match => string.IsNullOrEmpty(match.Trim()));
|
||||
|
||||
if (matches.Count == 0) return false;
|
||||
|
||||
if (matches.Count == 1)
|
||||
{
|
||||
rawTitle = matches[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
rawTitle = matches[1];
|
||||
}
|
||||
|
||||
(rawTitle, _) = SplitTitleAndSeason(rawTitle);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseSeason(Item item, out string season)
|
||||
{
|
||||
season = null;
|
||||
var name = item.Name();
|
||||
var matches = Regex.Matches(name, @"[^\[\]_]+").Select(match => match.Value).ToArray();
|
||||
if (matches.Length == 0) return false;
|
||||
|
||||
if (matches.Length == 1)
|
||||
{
|
||||
(_, season) = SplitTitleAndSeason(matches[0]);
|
||||
if (!string.IsNullOrEmpty(season)) return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
(_, season) = SplitTitleAndSeason(matches[1]);
|
||||
if (!string.IsNullOrEmpty(season)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryParseGroup(Item item, out string group)
|
||||
{
|
||||
group = null;
|
||||
var name = item.Name();
|
||||
var matches = Regex.Matches(name, @"[^\[\]_《》【】]+").Select(match => match.Value).ToArray();
|
||||
if (matches.Length <= 1) return false;
|
||||
group = matches[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private delegate bool FieldParser(Item item, out string result);
|
||||
|
||||
private bool TryParseField(TreeNode node, FieldParser fieldParser, out string result)
|
||||
{
|
||||
result = null;
|
||||
if (node.Info == null) return false;
|
||||
if (!fieldParser(node.Info, out var fieldValue)) return false;
|
||||
var parsed = new List<string>();
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
if (fieldParser(child.Info, out var childFieldValue))
|
||||
{
|
||||
parsed.Add(childFieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
var totalCount = parsed.Count;
|
||||
if (totalCount == 0)
|
||||
{
|
||||
result = fieldValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
var maxGroupCount = parsed.GroupBy(value => value)
|
||||
.Select(group => group.Count())
|
||||
.Max();
|
||||
|
||||
if (maxGroupCount > totalCount / 2)
|
||||
{
|
||||
result = fieldValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DoParse(TreeNode node, FieldParser fieldParser, string fieldName)
|
||||
{
|
||||
var queue = new Queue<TreeNode>();
|
||||
queue.Enqueue(node);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (TryParseField(current, fieldParser, out var fieldValue))
|
||||
{
|
||||
current.Info.Info.TryAdd(fieldName, fieldValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var child in current.Children)
|
||||
{
|
||||
queue.Enqueue(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Parse(TreeNode node)
|
||||
{
|
||||
DoParse(node, TryParseRawTitle, ItemFields.Key_RawTitle);
|
||||
DoParse(node, TryParseSeason, ItemFields.Key_Season);
|
||||
DoParse(node, TryParseGroup, ItemFields.Key_Group);
|
||||
}
|
||||
}
|
||||
1
Parsers/RawParser.cs.uid
Normal file
1
Parsers/RawParser.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://w0q201bv7vj8
|
||||
@ -52,90 +52,38 @@ public class TMDBParser(Configs configs) : ItemParser
|
||||
return _client;
|
||||
}
|
||||
|
||||
private string ParseSeason(string season)
|
||||
{
|
||||
if (int.TryParse(season, out _)) return season;
|
||||
|
||||
switch (season)
|
||||
{
|
||||
case "零": return "0";
|
||||
case "一": return "1";
|
||||
case "二": return "2";
|
||||
case "三": return "3";
|
||||
case "四": return "4";
|
||||
case "五": return "5";
|
||||
case "六": return "6";
|
||||
case "七": return "7";
|
||||
case "八": return "8";
|
||||
case "九": return "9";
|
||||
}
|
||||
return season;
|
||||
}
|
||||
|
||||
private bool DoRawParse(TreeNode node)
|
||||
{
|
||||
var item = node.Info;
|
||||
var name = item.Name();
|
||||
var matches = Regex.Matches(name, @"[^\[\]_]+").Select(match => match.Value).ToArray();
|
||||
|
||||
if (matches.Length == 0) return false;
|
||||
|
||||
if (matches.Length == 1)
|
||||
{
|
||||
node.Info.SetRawTitleIfNotExist(matches[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (matches.Length >= 2)
|
||||
{
|
||||
node.Info.SetGroupIfNotExist(matches[0]);
|
||||
|
||||
var title = matches[1].Trim();
|
||||
var match = Regex.Match(title, "(.+)第(.+)季");
|
||||
if (match.Success)
|
||||
{
|
||||
node.Info.SetSeasonIfNotExist(ParseSeason(match.Groups[2].Value.Trim()));
|
||||
node.Info.SetRawTitleIfNotExist(match.Groups[1].Value.Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
node.Info.SetRawTitleIfNotExist(title);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DoParse(TreeNode node)
|
||||
|
||||
public async Task Parse(TreeNode node)
|
||||
{
|
||||
if (!node.TryGetValue(ItemFields.Key_RawTitle, out var rawTitle, out _))
|
||||
{
|
||||
if (!DoRawParse(node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
node.TryGetValue(ItemFields.Key_RawTitle, out rawTitle, out _);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rawTitle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await QueryTMDB(rawTitle);
|
||||
if (result == null)
|
||||
{
|
||||
GD.PrintErr($"找不到对应的TV:{rawTitle}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.FirstAirDate != null)
|
||||
{
|
||||
node.Info.SetYear(result.FirstAirDate.Value.Year);
|
||||
}
|
||||
|
||||
node.Info.SetTitleIfNotExist(result.Name);
|
||||
|
||||
return true;
|
||||
// if (!node.TryGetValue(ItemFields.Key_RawTitle, out var rawTitle, out _))
|
||||
// {
|
||||
// if (!DoRawParse(node))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// node.TryGetValue(ItemFields.Key_RawTitle, out rawTitle, out _);
|
||||
// }
|
||||
//
|
||||
// if (string.IsNullOrEmpty(rawTitle))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// var result = await QueryTMDB(rawTitle);
|
||||
// if (result == null)
|
||||
// {
|
||||
// GD.PrintErr($"找不到对应的TV:{rawTitle}");
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// if (result.FirstAirDate != null)
|
||||
// {
|
||||
// node.Info.SetYear(result.FirstAirDate.Value.Year);
|
||||
// }
|
||||
//
|
||||
// node.Info.SetTitleIfNotExist(result.Name);
|
||||
//
|
||||
// return true;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user