aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Books/ComicInfo/ComicInfoReader.cs
blob: 4e8dc405ecdad3c2fc823d5ad2bcadb6e69aecd2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;

namespace MediaBrowser.Providers.Books.ComicInfo;

/// <summary>
/// ComicInfo reader.
/// </summary>
public static class ComicInfoReader
{
    /// <summary>
    /// Filename to check for comic metadata either next to the comic file or inside the archive.
    /// </summary>
    public const string ComicRackMetaFile = "ComicInfo.xml";

    /// <summary>
    /// Read comic book metadata.
    /// </summary>
    /// <param name="xml">The XDocument to read for comic metadata.</param>
    /// <returns>The resulting book.</returns>
    public static Book? ReadComicBookMetadata(XDocument xml)
    {
        var book = new Book();
        var hasFoundMetadata = false;

        // this value is only used internally since Jellyfin has no manga flag
        var isManga = false;

        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Title", title => book.Name = title);
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Manga", manga => isManga = manga.Equals("Yes", StringComparison.OrdinalIgnoreCase));
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Series", series => book.SeriesName = series);
        hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Number", issue => book.IndexNumber = issue);
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Summary", summary => book.Overview = summary);
        hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Year", year => book.ProductionYear = year);
        hasFoundMetadata |= ReadThreePartDateInto(xml, "ComicInfo/Year", "ComicInfo/Month", "ComicInfo/Day", dateTime => book.PremiereDate = dateTime);
        hasFoundMetadata |= ReadCommaSeparatedStringsInto(xml, "ComicInfo/Genre", genres =>
        {
            foreach (var genre in genres)
            {
                book.AddGenre(genre);
            }
        });
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Publisher", publisher => book.SetStudios([publisher]));

        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/AlternateSeries", title =>
        {
            if (isManga)
            {
                // Software like ComicTagger (https://github.com/comictagger/comictagger) will use
                // this field for the series name in the original language when tagging manga.
                book.OriginalTitle = title;
            }
            else
            {
                // Some US comics can be part of cross-over story arcs. This field is then used to
                // specify an alternate series.
            }
        });

        return hasFoundMetadata ? book : null;
    }

    /// <summary>
    /// Read people metadata.
    /// </summary>
    /// <param name="xml">The XDocument to read for people metadata.</param>
    /// <param name="metadataResult">The metadata result to update.</param>
    public static void ReadPeopleMetadata(XDocument xml, MetadataResult<Book> metadataResult)
    {
        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Writer", authors =>
        {
            foreach (var p in authors)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Author });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Penciller", pencillers =>
        {
            foreach (var p in pencillers)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Penciller });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Inker", inkers =>
        {
            foreach (var p in inkers)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Inker });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Letterer", letterers =>
        {
            foreach (var p in letterers)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Letterer });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/CoverArtist", artists =>
        {
            foreach (var p in artists)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.CoverArtist });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Colourist", colorists =>
        {
            foreach (var p in colorists)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Colorist });
            }
        });
    }

    /// <summary>
    /// Read culture information.
    /// </summary>
    /// <param name="xml">the XDocument to read for metadata.</param>
    /// <param name="xPath">The path to search.</param>
    /// <param name="commitResult">The action to take after parsing all metadata.</param>
    public static void ReadCultureInfoInto(XDocument xml, string xPath, Action<CultureInfo> commitResult)
    {
        string? culture = null;

        if (!ReadStringInto(xml, xPath, value => culture = value))
        {
            return;
        }

        // culture cannot be null here as the method would have returned earlier
        commitResult(new CultureInfo(culture!));
    }

    private static bool ReadStringInto(XDocument xml, string xPath, Action<string> commitResult)
    {
        var resultElement = xml.XPathSelectElement(xPath);

        if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.Value))
        {
            commitResult(resultElement.Value);
            return true;
        }

        return false;
    }

    private static bool ReadCommaSeparatedStringsInto(XDocument xml, string xPath, Action<IEnumerable<string>> commitResult)
    {
        var resultElement = xml.XPathSelectElement(xPath);

        if (resultElement is null || string.IsNullOrWhiteSpace(resultElement.Value))
        {
            return false;
        }

        try
        {
            var splits = resultElement.Value.Split(",").Select(p => p.Trim()).ToArray();
            if (splits.Length < 1)
            {
                return false;
            }

            commitResult(splits);
            return true;
        }
        catch (ArgumentNullException)
        {
            return false;
        }
    }

    private static bool ReadIntInto(XDocument xml, string xPath, Action<int> commitResult)
    {
        var resultElement = xml.XPathSelectElement(xPath);

        if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.Value))
        {
            return ParseInt(resultElement.Value, commitResult);
        }

        return false;
    }

    private static bool ReadThreePartDateInto(XDocument xml, string yearXPath, string monthXPath, string dayXPath, Action<DateTime> commitResult)
    {
        int year = 0;
        int month = 0;
        int day = 0;
        var parsed = false;

        parsed |= ReadIntInto(xml, yearXPath, num => year = num);
        parsed |= ReadIntInto(xml, monthXPath, num => month = num);
        parsed |= ReadIntInto(xml, dayXPath, num => day = num);

        if (!parsed)
        {
            return false;
        }

        try
        {
            var dateTime = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Unspecified);

            commitResult(dateTime);
            return true;
        }
        catch (ArgumentOutOfRangeException)
        {
            return false;
        }
    }

    private static bool ParseInt(string input, Action<int> commitResult)
    {
        if (int.TryParse(input, out var parsed))
        {
            commitResult(parsed);
            return true;
        }

        return false;
    }
}