├── .gitattributes
├── .gitignore
├── .nuget
├── NuGet.Config
├── NuGet.exe
└── NuGet.targets
├── MalApi.Example
├── App.config
├── MalApi.Example.csproj
├── Program.cs
├── Properties
│ └── AssemblyInfo.cs
└── packages.config
├── MalApi.IntegrationTests
├── GetAnimeDetailsTest.cs
├── GetAnimeListForUserTest.cs
├── GetRecentOnlineUsersTest.cs
├── MalApi.IntegrationTests.csproj
├── Properties
│ └── AssemblyInfo.cs
└── readme.txt
├── MalApi.NetCoreExample
├── MalApi.NetCoreExample.csproj
└── Program.cs
├── MalApi.UnitTests
├── AnimeListCacheTests.cs
├── Eureka_Seven.htm
├── Helpers.cs
├── MalApi.UnitTests.csproj
├── MalAppInfoXmlTests.cs
├── MyAnimeListApiTests.cs
├── Properties
│ └── AssemblyInfo.cs
├── test.xml
├── test_clean.xml
├── test_error.xml
├── test_no_such_user.xml
└── test_no_such_user_old.xml
├── MalApi.sln
├── MalApi
├── AnimeDetailsResults.cs
├── AnimeListCache.cs
├── AssemblyInfo.cs
├── CachingMyAnimeListApi.cs
├── CompletionStatus.cs
├── Genre.cs
├── IMyAnimeListApi.cs
├── Logging.cs
├── MalAnimeInfoFromUserLookup.cs
├── MalAnimeNotFoundException.cs
├── MalAnimeType.cs
├── MalApi.csproj
├── MalApiException.cs
├── MalApiRequestException.cs
├── MalAppInfoXml.cs
├── MalSeriesStatus.cs
├── MalUserLookupResults.cs
├── MalUserNotFoundException.cs
├── MyAnimeListApi.cs
├── MyAnimeListEntry.cs
├── Properties
│ └── AssemblyInfo.cs
├── RateLimitingMyAnimeListApi.cs
├── RecentUsersResults.cs
├── RetryOnFailureMyAnimeListApi.cs
├── UncertainDate.cs
├── packages.config
└── readme.txt
├── license.txt
├── notice.txt
├── packages
└── repositories.config
└── readme.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | *.user
4 | *.suo
5 | *.pubxml
6 | packages/*/
7 | MalApi/package/
--------------------------------------------------------------------------------
/.nuget/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.nuget/NuGet.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LHCGreg/mal-api/edc93e5be5b8cc33970c762dc44f013d27666c0f/.nuget/NuGet.exe
--------------------------------------------------------------------------------
/.nuget/NuGet.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildProjectDirectory)\..\
5 |
6 |
7 | false
8 |
9 |
10 | false
11 |
12 |
13 | true
14 |
15 |
16 | false
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
31 |
32 |
33 |
34 |
35 | $(SolutionDir).nuget
36 |
37 |
38 |
39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config
40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config
41 |
42 |
43 |
44 | $(MSBuildProjectDirectory)\packages.config
45 | $(PackagesProjectConfig)
46 |
47 |
48 |
49 |
50 | $(NuGetToolsPath)\NuGet.exe
51 | @(PackageSource)
52 |
53 | "$(NuGetExePath)"
54 | mono --runtime=v4.0.30319 "$(NuGetExePath)"
55 |
56 | $(TargetDir.Trim('\\'))
57 |
58 | -RequireConsent
59 | -NonInteractive
60 |
61 | "$(SolutionDir) "
62 | "$(SolutionDir)"
63 |
64 |
65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)
66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols
67 |
68 |
69 |
70 | RestorePackages;
71 | $(BuildDependsOn);
72 |
73 |
74 |
75 |
76 | $(BuildDependsOn);
77 | BuildPackage;
78 |
79 |
80 |
81 |
82 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
99 |
100 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/MalApi.Example/App.config:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/MalApi.Example/MalApi.Example.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.30703
7 | 2.0
8 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}
9 | Exe
10 | Properties
11 | MalApi.Example
12 | MalApi.Example
13 | v4.6.2
14 |
15 |
16 | 512
17 | ..\
18 | true
19 |
20 |
21 | true
22 | bin\Debug\
23 | DEBUG;TRACE
24 | full
25 | AnyCPU
26 | prompt
27 | false
28 | false
29 | false
30 |
31 |
32 | bin\Release\
33 | TRACE
34 | true
35 | pdbonly
36 | AnyCPU
37 | prompt
38 | false
39 | false
40 |
41 |
42 |
43 | ..\packages\Common.Logging.3.4.0-Beta2\lib\net40\Common.Logging.dll
44 |
45 |
46 | ..\packages\Common.Logging.Core.3.4.0-Beta2\lib\net40\Common.Logging.Core.dll
47 |
48 |
49 | ..\packages\Common.Logging.NLog41.3.4.0-Beta2\lib\net40\Common.Logging.NLog41.dll
50 |
51 |
52 |
53 | ..\packages\NLog.4.4.5\lib\net45\NLog.dll
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {85BC477F-4798-4006-A342-715E7565B3BB}
65 | MalApi
66 |
67 |
68 |
69 |
70 | Designer
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/MalApi.Example/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi.Example
7 | {
8 | class Program
9 | {
10 | static void Main(string[] args)
11 | {
12 | // MalApi uses the Common.Logging logging abstraction.
13 | // You can hook it up to any logging library that has a Common.Logging adapter.
14 | // See App.config for an example of hooking up MalApi to NLog.
15 | // Note that you will also need the appropriate NLog and Common.Logging.NLogXX packages installed.
16 | // Hooking up logging is not necessary but can be useful.
17 | // With the configuration in this example and with this example program, you will see lines like:
18 |
19 | // Logged from MalApi: Getting anime list for MAL user LordHighCaptain using URI https://myanimelist.net/malappinfo.php?status=all&type=anime&u=LordHighCaptain
20 | // Logged from MalApi: Successfully retrieved anime list for user LordHighCaptain
21 |
22 | using (MyAnimeListApi api = new MyAnimeListApi())
23 | {
24 | api.UserAgent = "MalApiExample";
25 | api.TimeoutInMs = 15000;
26 |
27 | MalUserLookupResults userLookup = api.GetAnimeListForUser("LordHighCaptain");
28 | foreach (MyAnimeListEntry listEntry in userLookup.AnimeList)
29 | {
30 | Console.WriteLine("Rating for {0}: {1}", listEntry.AnimeInfo.Title, listEntry.Score);
31 | }
32 |
33 | Console.WriteLine();
34 | Console.WriteLine();
35 |
36 | RecentUsersResults recentUsersResults = api.GetRecentOnlineUsers();
37 | foreach (string user in recentUsersResults.RecentUsers)
38 | {
39 | Console.WriteLine("Recent user: {0}", user);
40 | }
41 |
42 | Console.WriteLine();
43 | Console.WriteLine();
44 |
45 | int eurekaSevenID = 237;
46 | AnimeDetailsResults eurekaSeven = api.GetAnimeDetails(eurekaSevenID);
47 | Console.WriteLine("Eureka Seven genres: {0}", string.Join(", ", eurekaSeven.Genres));
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/MalApi.Example/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("MalApi.Example")]
9 | [assembly: AssemblyDescription("Program demonstrating use of MalApi")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("MAL API")]
13 | [assembly: AssemblyCopyright("Copyright © Greg Najda 2012")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("7af58b87-0b39-457a-a43e-bb80a1dc11a8")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/MalApi.Example/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MalApi.IntegrationTests/GetAnimeDetailsTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using FluentAssertions;
8 | using Xunit;
9 |
10 | namespace MalApi.IntegrationTests
11 | {
12 | public class GetAnimeDetailsTest
13 | {
14 | [Fact]
15 | public void GetAnimeDetails()
16 | {
17 | int animeId = 237; // Eureka Seven
18 | using (MyAnimeListApi api = new MyAnimeListApi())
19 | {
20 | AnimeDetailsResults results = api.GetAnimeDetails(animeId);
21 | List expectedGenres = new List()
22 | {
23 | new Genre(2, "Adventure"),
24 | new Genre(8, "Drama"),
25 | new Genre(18, "Mecha"),
26 | new Genre(22, "Romance"),
27 | new Genre(24, "Sci-Fi"),
28 | };
29 | results.Genres.Should().BeEquivalentTo(expectedGenres);
30 | }
31 | }
32 |
33 | [Fact]
34 | public void GetAnimeDetailsCanceled()
35 | {
36 | int animeId = 237; // Eureka Seven
37 | using (MyAnimeListApi api = new MyAnimeListApi())
38 | {
39 | CancellationTokenSource tokenSource = new CancellationTokenSource();
40 | Task task = api.GetAnimeDetailsAsync(animeId, tokenSource.Token);
41 | tokenSource.Cancel();
42 | Assert.Throws(() => task.GetAwaiter().GetResult());
43 | }
44 | }
45 |
46 | [Fact]
47 | public void GetAnimeDetailsForInvalidAnimeId()
48 | {
49 | int animeId = 99999;
50 | using (MyAnimeListApi api = new MyAnimeListApi())
51 | {
52 | Assert.Throws(() => api.GetAnimeDetails(animeId));
53 | }
54 | }
55 | }
56 | }
57 |
58 | /*
59 | Copyright 2017 Greg Najda
60 |
61 | Licensed under the Apache License, Version 2.0 (the "License");
62 | you may not use this file except in compliance with the License.
63 | You may obtain a copy of the License at
64 |
65 | http://www.apache.org/licenses/LICENSE-2.0
66 |
67 | Unless required by applicable law or agreed to in writing, software
68 | distributed under the License is distributed on an "AS IS" BASIS,
69 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
70 | See the License for the specific language governing permissions and
71 | limitations under the License.
72 | */
--------------------------------------------------------------------------------
/MalApi.IntegrationTests/GetAnimeListForUserTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using MalApi;
6 | using System.Threading.Tasks;
7 | using Xunit;
8 | using System.Threading;
9 |
10 | namespace MalApi.IntegrationTests
11 | {
12 | public class GetAnimeListForUserTest
13 | {
14 | [Fact]
15 | public void GetAnimeListForUser()
16 | {
17 | string username = "lordhighcaptain";
18 | using (MyAnimeListApi api = new MyAnimeListApi())
19 | {
20 | MalUserLookupResults userLookup = api.GetAnimeListForUser(username);
21 |
22 | // Just a smoke test that checks that getting an anime list returns something
23 | Assert.NotEmpty(userLookup.AnimeList);
24 | }
25 | }
26 |
27 | [Fact]
28 | public void GetAnimeListForUserCanceled()
29 | {
30 | string username = "lordhighcaptain";
31 | using (MyAnimeListApi api = new MyAnimeListApi())
32 | {
33 | CancellationTokenSource tokenSource = new CancellationTokenSource();
34 | Task userLookupTask = api.GetAnimeListForUserAsync(username, tokenSource.Token);
35 | tokenSource.Cancel();
36 | Assert.Throws(() => userLookupTask.GetAwaiter().GetResult());
37 | }
38 | }
39 |
40 | [Fact]
41 | public void GetAnimeListForNonexistentUserThrowsCorrectException()
42 | {
43 | using (MyAnimeListApi api = new MyAnimeListApi())
44 | {
45 | Assert.Throws(() => api.GetAnimeListForUser("oijsfjisfdjfsdojpfsdp"));
46 | }
47 | }
48 |
49 | [Fact]
50 | public void GetAnimeListForNonexistentUserThrowsCorrectExceptionAsync()
51 | {
52 | using (MyAnimeListApi api = new MyAnimeListApi())
53 | {
54 | Assert.ThrowsAsync(() => api.GetAnimeListForUserAsync("oijsfjisfdjfsdojpfsdp"));
55 | }
56 | }
57 | }
58 | }
59 |
60 | /*
61 | Copyright 2017 Greg Najda
62 |
63 | Licensed under the Apache License, Version 2.0 (the "License");
64 | you may not use this file except in compliance with the License.
65 | You may obtain a copy of the License at
66 |
67 | http://www.apache.org/licenses/LICENSE-2.0
68 |
69 | Unless required by applicable law or agreed to in writing, software
70 | distributed under the License is distributed on an "AS IS" BASIS,
71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72 | See the License for the specific language governing permissions and
73 | limitations under the License.
74 | */
--------------------------------------------------------------------------------
/MalApi.IntegrationTests/GetRecentOnlineUsersTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Xunit;
8 |
9 | namespace MalApi.IntegrationTests
10 | {
11 | public class GetRecentOnlineUsersTest
12 | {
13 | [Fact]
14 | public void GetRecentOnlineUsers()
15 | {
16 | using (MyAnimeListApi api = new MyAnimeListApi())
17 | {
18 | RecentUsersResults results = api.GetRecentOnlineUsers();
19 | Assert.NotEmpty(results.RecentUsers);
20 | }
21 | }
22 |
23 | [Fact]
24 | public void GetRecentOnlineUsersCanceled()
25 | {
26 | using (MyAnimeListApi api = new MyAnimeListApi())
27 | {
28 | CancellationTokenSource tokenSource = new CancellationTokenSource();
29 | Task task = api.GetRecentOnlineUsersAsync(tokenSource.Token);
30 | tokenSource.Cancel();
31 | Assert.Throws(() => task.GetAwaiter().GetResult());
32 | }
33 | }
34 | }
35 | }
36 |
37 | /*
38 | Copyright 2017 Greg Najda
39 |
40 | Licensed under the Apache License, Version 2.0 (the "License");
41 | you may not use this file except in compliance with the License.
42 | You may obtain a copy of the License at
43 |
44 | http://www.apache.org/licenses/LICENSE-2.0
45 |
46 | Unless required by applicable law or agreed to in writing, software
47 | distributed under the License is distributed on an "AS IS" BASIS,
48 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
49 | See the License for the specific language governing permissions and
50 | limitations under the License.
51 | */
--------------------------------------------------------------------------------
/MalApi.IntegrationTests/MalApi.IntegrationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net452;netcoreapp1.0
5 | MalApi.IntegrationTests
6 | MalApi.IntegrationTests
7 | 4.0.0.0
8 | Integration tests for MalApi
9 | true
10 | true
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/MalApi.IntegrationTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyCopyright("Copyright © Greg Najda 2012")]
9 | [assembly: AssemblyTrademark("")]
10 | [assembly: AssemblyCulture("")]
11 |
12 | // Setting ComVisible to false makes the types in this assembly not visible
13 | // to COM components. If you need to access a type in this assembly from
14 | // COM, set the ComVisible attribute to true on that type.
15 | [assembly: ComVisible(false)]
--------------------------------------------------------------------------------
/MalApi.IntegrationTests/readme.txt:
--------------------------------------------------------------------------------
1 | No special setup is needed in order to run these tests. MAL seems to no longer require an API key.
--------------------------------------------------------------------------------
/MalApi.NetCoreExample/MalApi.NetCoreExample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp1.1
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/MalApi.NetCoreExample/Program.cs:
--------------------------------------------------------------------------------
1 | using MalApi;
2 | using System;
3 |
4 | namespace MalApi.NetCoreExample
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | // No logging support for MalApi in .NET Core yet.
11 | // At this time there is no Common.Logging adapter for NLog that supports .NET core.
12 | using (MyAnimeListApi api = new MyAnimeListApi())
13 | {
14 | api.UserAgent = "MalApiExample";
15 | api.TimeoutInMs = 15000;
16 |
17 | MalUserLookupResults userLookup = api.GetAnimeListForUser("LordHighCaptain");
18 | foreach (MyAnimeListEntry listEntry in userLookup.AnimeList)
19 | {
20 | Console.WriteLine("Rating for {0}: {1}", listEntry.AnimeInfo.Title, listEntry.Score);
21 | }
22 |
23 | Console.WriteLine();
24 | Console.WriteLine();
25 |
26 | RecentUsersResults recentUsersResults = api.GetRecentOnlineUsers();
27 | foreach (string user in recentUsersResults.RecentUsers)
28 | {
29 | Console.WriteLine("Recent user: {0}", user);
30 | }
31 |
32 | Console.WriteLine();
33 | Console.WriteLine();
34 |
35 | int eurekaSevenID = 237;
36 | AnimeDetailsResults eurekaSeven = api.GetAnimeDetails(eurekaSevenID);
37 | Console.WriteLine("Eureka Seven genres: {0}", string.Join(", ", eurekaSeven.Genres));
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/MalApi.UnitTests/AnimeListCacheTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using MalApi;
6 | using System.Threading;
7 | using Xunit;
8 |
9 | namespace MalApi.UnitTests
10 | {
11 | public class AnimeListCacheTests
12 | {
13 | [Fact]
14 | public void TestCacheCaseInsensitivity()
15 | {
16 | using (AnimeListCache cache = new AnimeListCache(expiration: TimeSpan.FromHours(5)))
17 | {
18 | cache.PutListForUser("a", new MalUserLookupResults(userId: 5, canonicalUserName: "A", animeList: new List()));
19 | cache.GetListForUser("A", out MalUserLookupResults lookup);
20 | Assert.Equal(5, lookup.UserId);
21 | }
22 | }
23 | }
24 | }
25 |
26 | /*
27 | Copyright 2017 Greg Najda
28 |
29 | Licensed under the Apache License, Version 2.0 (the "License");
30 | you may not use this file except in compliance with the License.
31 | You may obtain a copy of the License at
32 |
33 | http://www.apache.org/licenses/LICENSE-2.0
34 |
35 | Unless required by applicable law or agreed to in writing, software
36 | distributed under the License is distributed on an "AS IS" BASIS,
37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38 | See the License for the specific language governing permissions and
39 | limitations under the License.
40 | */
--------------------------------------------------------------------------------
/MalApi.UnitTests/Helpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace MalApi.UnitTests
10 | {
11 | static class Helpers
12 | {
13 | private static string GetResourceName(string fileName)
14 | {
15 | return "MalApi.UnitTests." + fileName;
16 | }
17 |
18 | public static StreamReader GetResourceStream(string fileName)
19 | {
20 | string resourceName = GetResourceName(fileName);
21 | return new StreamReader(typeof(Helpers).GetTypeInfo().Assembly.GetManifestResourceStream(resourceName), Encoding.UTF8);
22 | }
23 |
24 | public static string GetResourceText(string fileName)
25 | {
26 | using (StreamReader resourceStream = GetResourceStream(fileName))
27 | {
28 | return resourceStream.ReadToEnd();
29 | }
30 | }
31 | }
32 | }
33 |
34 | /*
35 | Copyright 2017 Greg Najda
36 |
37 | Licensed under the Apache License, Version 2.0 (the "License");
38 | you may not use this file except in compliance with the License.
39 | You may obtain a copy of the License at
40 |
41 | http://www.apache.org/licenses/LICENSE-2.0
42 |
43 | Unless required by applicable law or agreed to in writing, software
44 | distributed under the License is distributed on an "AS IS" BASIS,
45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
46 | See the License for the specific language governing permissions and
47 | limitations under the License.
48 | */
49 |
--------------------------------------------------------------------------------
/MalApi.UnitTests/MalApi.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net452;netcoreapp1.0
5 | MalApi.UnitTests
6 | MalApi.UnitTests
7 | 4.0.0.0
8 | Unit tests for MalApi
9 | true
10 | true
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 |
--------------------------------------------------------------------------------
/MalApi.UnitTests/MalAppInfoXmlTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.IO;
6 | using System.Xml.Linq;
7 | using FluentAssertions;
8 | using Xunit;
9 |
10 | namespace MalApi.UnitTests
11 | {
12 | public partial class MalAppInfoXmlTests
13 | {
14 | [Fact]
15 | public void ParseWithTextReaderTest()
16 | {
17 | using (TextReader reader = Helpers.GetResourceStream("test.xml"))
18 | {
19 | MalUserLookupResults results = MalAppInfoXml.Parse(reader);
20 | DoAsserts(results);
21 | }
22 | }
23 |
24 | [Fact]
25 | public void ParseWithXElementTest()
26 | {
27 | XDocument doc = XDocument.Parse(Helpers.GetResourceText("test_clean.xml"));
28 | MalUserLookupResults results = MalAppInfoXml.Parse(doc);
29 | DoAsserts(results);
30 | }
31 |
32 | [Fact]
33 | public void ParseInvalidUserWithTextReaderTest()
34 | {
35 | using (TextReader reader = Helpers.GetResourceStream("test_no_such_user.xml"))
36 | {
37 | Assert.Throws(() => MalAppInfoXml.Parse(reader));
38 | }
39 | }
40 |
41 | [Fact]
42 | public void ParseInvalidUserWithXElementTest()
43 | {
44 | XDocument doc = XDocument.Parse(Helpers.GetResourceText("test_no_such_user.xml"));
45 | Assert.Throws(() => MalAppInfoXml.Parse(doc));
46 | }
47 |
48 | [Fact]
49 | public void ParseOldInvalidUserWithTextReaderTest()
50 | {
51 | using (TextReader reader = Helpers.GetResourceStream("test_no_such_user_old.xml"))
52 | {
53 | Assert.Throws(() => MalAppInfoXml.Parse(reader));
54 | }
55 | }
56 |
57 | [Fact]
58 | public void ParseOldInvalidUserWithXElementTest()
59 | {
60 | XDocument doc = XDocument.Parse(Helpers.GetResourceText("test_no_such_user_old.xml"));
61 | Assert.Throws(() => MalAppInfoXml.Parse(doc));
62 | }
63 |
64 | private void DoAsserts(MalUserLookupResults results)
65 | {
66 | Assert.Equal("LordHighCaptain", results.CanonicalUserName);
67 | Assert.Equal(158667, results.UserId);
68 | Assert.Equal(163, results.AnimeList.Count);
69 |
70 | MyAnimeListEntry entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 853).First();
71 | Assert.Equal("Ouran Koukou Host Club", entry.AnimeInfo.Title);
72 | Assert.Equal(MalAnimeType.Tv, entry.AnimeInfo.Type);
73 | entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List() { "Ohran Koko Host Club", "Ouran Koukou Hosutobu", "Ouran High School Host Club" });
74 |
75 | Assert.Equal(7, entry.NumEpisodesWatched);
76 | Assert.Equal(7, entry.Score);
77 | Assert.Equal(CompletionStatus.Watching, entry.Status);
78 |
79 | // Test tags with Equal, not equivalent, because order in tags matters
80 | Assert.Equal(new List() { "duck", "goose" }, entry.Tags);
81 |
82 | entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 7311).First();
83 | Assert.Equal("Suzumiya Haruhi no Shoushitsu", entry.AnimeInfo.Title);
84 | Assert.Equal(MalAnimeType.Movie, entry.AnimeInfo.Type);
85 | entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List() { "The Vanishment of Haruhi Suzumiya", "Suzumiya Haruhi no Syoshitsu", "Haruhi movie", "The Disappearance of Haruhi Suzumiya" });
86 | Assert.Equal((decimal?)null, entry.Score);
87 | Assert.Equal(0, entry.NumEpisodesWatched);
88 | Assert.Equal(CompletionStatus.PlanToWatch, entry.Status);
89 | Assert.Equal(new List(), entry.Tags);
90 |
91 | entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 889).First();
92 | Assert.Equal("Black Lagoon", entry.AnimeInfo.Title);
93 |
94 | // Make sure synonyms that are the same as the real name get filtered out
95 | entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List());
96 |
97 | entry = results.AnimeList.Where(anime => anime.AnimeInfo.Title == "Test").First();
98 | // Make sure that is the same as
99 | entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List());
100 | Assert.Equal(new UncertainDate(2010, 2, 6), entry.AnimeInfo.StartDate);
101 | Assert.Equal(UncertainDate.Unknown, entry.AnimeInfo.EndDate);
102 | Assert.Equal("https://cdn.myanimelist.net/images/anime/9/24646.jpg", entry.AnimeInfo.ImageUrl);
103 | Assert.Equal(new UncertainDate(year: null, month: 2, day: null), entry.MyStartDate);
104 | Assert.Equal(UncertainDate.Unknown, entry.MyFinishDate);
105 | Assert.Equal(new DateTime(year: 2011, month: 4, day: 2, hour: 22, minute: 50, second: 58, kind: DateTimeKind.Utc), entry.MyLastUpdate);
106 | Assert.Equal(new List() { "test&test", "< less than", "> greater than", "apos '", "quote \"", "hex ö", "dec !", "control character" }, entry.Tags);
107 |
108 | }
109 | }
110 | }
111 |
112 | /*
113 | Copyright 2017 Greg Najda
114 |
115 | Licensed under the Apache License, Version 2.0 (the "License");
116 | you may not use this file except in compliance with the License.
117 | You may obtain a copy of the License at
118 |
119 | http://www.apache.org/licenses/LICENSE-2.0
120 |
121 | Unless required by applicable law or agreed to in writing, software
122 | distributed under the License is distributed on an "AS IS" BASIS,
123 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
124 | See the License for the specific language governing permissions and
125 | limitations under the License.
126 | */
127 |
--------------------------------------------------------------------------------
/MalApi.UnitTests/MyAnimeListApiTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Reflection;
6 | using System.IO;
7 | using FluentAssertions;
8 | using Xunit;
9 |
10 | namespace MalApi.UnitTests
11 | {
12 | public class MyAnimeListApiTests
13 | {
14 | [Fact]
15 | public void TestScrapeAnimeDetailsFromHtml()
16 | {
17 | string html;
18 | using (StreamReader reader = Helpers.GetResourceStream("Eureka_Seven.htm"))
19 | {
20 | html = reader.ReadToEnd();
21 | }
22 |
23 | using (MyAnimeListApi api = new MyAnimeListApi())
24 | {
25 | AnimeDetailsResults results = api.ScrapeAnimeDetailsFromHtml(html, 237);
26 | List expectedGenres = new List()
27 | {
28 | new Genre(2, "Adventure"),
29 | new Genre(8, "Drama"),
30 | new Genre(18, "Mecha"),
31 | new Genre(22, "Romance"),
32 | new Genre(24, "Sci-Fi"),
33 | };
34 | results.Genres.Should().BeEquivalentTo(expectedGenres);
35 | }
36 | }
37 | }
38 | }
39 |
40 | /*
41 | Copyright 2017 Greg Najda
42 |
43 | Licensed under the Apache License, Version 2.0 (the "License");
44 | you may not use this file except in compliance with the License.
45 | You may obtain a copy of the License at
46 |
47 | http://www.apache.org/licenses/LICENSE-2.0
48 |
49 | Unless required by applicable law or agreed to in writing, software
50 | distributed under the License is distributed on an "AS IS" BASIS,
51 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52 | See the License for the specific language governing permissions and
53 | limitations under the License.
54 | */
55 |
--------------------------------------------------------------------------------
/MalApi.UnitTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyCopyright("Copyright © Greg Najda 2017")]
9 | [assembly: AssemblyTrademark("")]
10 | [assembly: AssemblyCulture("")]
11 |
12 | // Setting ComVisible to false makes the types in this assembly not visible
13 | // to COM components. If you need to access a type in this assembly from
14 | // COM, set the ComVisible attribute to true on that type.
15 | [assembly: ComVisible(false)]
16 |
--------------------------------------------------------------------------------
/MalApi.UnitTests/test_error.xml:
--------------------------------------------------------------------------------
1 |
2 | This site is busted!
--------------------------------------------------------------------------------
/MalApi.UnitTests/test_no_such_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/MalApi.UnitTests/test_no_such_user_old.xml:
--------------------------------------------------------------------------------
1 |
2 | Invalid username
--------------------------------------------------------------------------------
/MalApi.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26430.14
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MalApi", "MalApi\MalApi.csproj", "{85BC477F-4798-4006-A342-715E7565B3BB}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MalApi.UnitTests", "MalApi.UnitTests\MalApi.UnitTests.csproj", "{0D4FDB3D-1EBF-4242-B9DF-5650B846095C}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalApi.Example", "MalApi.Example\MalApi.Example.csproj", "{F1A0D744-4B00-4463-9CC7-91D57C1E4333}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MalApi.IntegrationTests", "MalApi.IntegrationTests\MalApi.IntegrationTests.csproj", "{4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{00247D56-2D9A-41AC-BE66-0E02E54DE0B8}"
15 | ProjectSection(SolutionItems) = preProject
16 | .nuget\NuGet.Config = .nuget\NuGet.Config
17 | .nuget\NuGet.exe = .nuget\NuGet.exe
18 | .nuget\NuGet.targets = .nuget\NuGet.targets
19 | EndProjectSection
20 | EndProject
21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalApi.NetCoreExample", "MalApi.NetCoreExample\MalApi.NetCoreExample.csproj", "{EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}"
22 | EndProject
23 | Global
24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
25 | Debug|Any CPU = Debug|Any CPU
26 | Debug|x64 = Debug|x64
27 | Debug|x86 = Debug|x86
28 | Release|Any CPU = Release|Any CPU
29 | Release|x64 = Release|x64
30 | Release|x86 = Release|x86
31 | EndGlobalSection
32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
33 | {85BC477F-4798-4006-A342-715E7565B3BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {85BC477F-4798-4006-A342-715E7565B3BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {85BC477F-4798-4006-A342-715E7565B3BB}.Debug|x64.ActiveCfg = Debug|Any CPU
36 | {85BC477F-4798-4006-A342-715E7565B3BB}.Debug|x64.Build.0 = Debug|Any CPU
37 | {85BC477F-4798-4006-A342-715E7565B3BB}.Debug|x86.ActiveCfg = Debug|Any CPU
38 | {85BC477F-4798-4006-A342-715E7565B3BB}.Debug|x86.Build.0 = Debug|Any CPU
39 | {85BC477F-4798-4006-A342-715E7565B3BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {85BC477F-4798-4006-A342-715E7565B3BB}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {85BC477F-4798-4006-A342-715E7565B3BB}.Release|x64.ActiveCfg = Release|Any CPU
42 | {85BC477F-4798-4006-A342-715E7565B3BB}.Release|x64.Build.0 = Release|Any CPU
43 | {85BC477F-4798-4006-A342-715E7565B3BB}.Release|x86.ActiveCfg = Release|Any CPU
44 | {85BC477F-4798-4006-A342-715E7565B3BB}.Release|x86.Build.0 = Release|Any CPU
45 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Debug|x64.ActiveCfg = Debug|Any CPU
48 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Debug|x64.Build.0 = Debug|Any CPU
49 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Debug|x86.ActiveCfg = Debug|Any CPU
50 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Debug|x86.Build.0 = Debug|Any CPU
51 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Release|Any CPU.Build.0 = Release|Any CPU
53 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Release|x64.ActiveCfg = Release|Any CPU
54 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Release|x64.Build.0 = Release|Any CPU
55 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Release|x86.ActiveCfg = Release|Any CPU
56 | {0D4FDB3D-1EBF-4242-B9DF-5650B846095C}.Release|x86.Build.0 = Release|Any CPU
57 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Debug|Any CPU.Build.0 = Debug|Any CPU
59 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Debug|x64.ActiveCfg = Debug|Any CPU
60 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Debug|x64.Build.0 = Debug|Any CPU
61 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Debug|x86.ActiveCfg = Debug|Any CPU
62 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Debug|x86.Build.0 = Debug|Any CPU
63 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Release|Any CPU.Build.0 = Release|Any CPU
65 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Release|x64.ActiveCfg = Release|Any CPU
66 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Release|x64.Build.0 = Release|Any CPU
67 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Release|x86.ActiveCfg = Release|Any CPU
68 | {F1A0D744-4B00-4463-9CC7-91D57C1E4333}.Release|x86.Build.0 = Release|Any CPU
69 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
70 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
71 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Debug|x64.ActiveCfg = Debug|Any CPU
72 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Debug|x64.Build.0 = Debug|Any CPU
73 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Debug|x86.ActiveCfg = Debug|Any CPU
74 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Debug|x86.Build.0 = Debug|Any CPU
75 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
76 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Release|Any CPU.Build.0 = Release|Any CPU
77 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Release|x64.ActiveCfg = Release|Any CPU
78 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Release|x64.Build.0 = Release|Any CPU
79 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Release|x86.ActiveCfg = Release|Any CPU
80 | {4CE0CE3E-BF07-4723-9A3B-786286AC5ECB}.Release|x86.Build.0 = Release|Any CPU
81 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
82 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
83 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Debug|x64.ActiveCfg = Debug|Any CPU
84 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Debug|x64.Build.0 = Debug|Any CPU
85 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Debug|x86.ActiveCfg = Debug|Any CPU
86 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Debug|x86.Build.0 = Debug|Any CPU
87 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
88 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Release|Any CPU.Build.0 = Release|Any CPU
89 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Release|x64.ActiveCfg = Release|Any CPU
90 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Release|x64.Build.0 = Release|Any CPU
91 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Release|x86.ActiveCfg = Release|Any CPU
92 | {EF72FAC1-AE34-4D7B-9839-4ED50288CFB4}.Release|x86.Build.0 = Release|Any CPU
93 | EndGlobalSection
94 | GlobalSection(SolutionProperties) = preSolution
95 | HideSolutionNode = FALSE
96 | EndGlobalSection
97 | EndGlobal
98 |
--------------------------------------------------------------------------------
/MalApi/AnimeDetailsResults.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public class AnimeDetailsResults
9 | {
10 | public IList Genres { get; private set; }
11 |
12 | public AnimeDetailsResults(IList genres)
13 | {
14 | Genres = genres;
15 | }
16 | }
17 | }
18 |
19 | /*
20 | Copyright 2012 Greg Najda
21 |
22 | Licensed under the Apache License, Version 2.0 (the "License");
23 | you may not use this file except in compliance with the License.
24 | You may obtain a copy of the License at
25 |
26 | http://www.apache.org/licenses/LICENSE-2.0
27 |
28 | Unless required by applicable law or agreed to in writing, software
29 | distributed under the License is distributed on an "AS IS" BASIS,
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 | See the License for the specific language governing permissions and
32 | limitations under the License.
33 | */
--------------------------------------------------------------------------------
/MalApi/AnimeListCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 |
7 | namespace MalApi
8 | {
9 | ///
10 | /// Thread-safe cache with an optional expiration time. If the expiration time is null, anime lists are cached for the lifetime of
11 | /// the object. Expired cache entries are only actually removed when a new anime list is inserted into the cache. Cache expiration
12 | /// measurement is susceptible to changes to the system clock.
13 | ///
14 | internal class AnimeListCache : IDisposable
15 | {
16 | private Dictionary m_animeListCache =
17 | new Dictionary(StringComparer.OrdinalIgnoreCase);
18 | private LinkedList> m_cachePutTimesSortedByTime;
19 | private Dictionary>> m_cachePutTimesByName;
20 | private TimeSpan? m_expiration;
21 |
22 | private ReaderWriterLockSlim m_cacheLock = new ReaderWriterLockSlim();
23 |
24 | public AnimeListCache(TimeSpan? expiration)
25 | {
26 | m_expiration = expiration;
27 | if (m_expiration != null)
28 | {
29 | m_cachePutTimesSortedByTime = new LinkedList>();
30 | m_cachePutTimesByName = new Dictionary>>(StringComparer.OrdinalIgnoreCase);
31 | }
32 | }
33 |
34 | public bool GetListForUser(string user, out MalUserLookupResults animeList)
35 | {
36 | m_cacheLock.EnterReadLock();
37 |
38 | try
39 | {
40 | if (m_expiration == null)
41 | {
42 | if (m_animeListCache.TryGetValue(user, out animeList))
43 | {
44 | return true;
45 | }
46 | else
47 | {
48 | animeList = null;
49 | return false;
50 | }
51 | }
52 |
53 | // Check if this user is in the cache and if the cache entry is not stale
54 | if (m_cachePutTimesByName.TryGetValue(user, out LinkedListNode> userAndTimeInsertedNode))
55 | {
56 | DateTime expirationTime = userAndTimeInsertedNode.Value.Item2 + m_expiration.Value;
57 | if (DateTime.UtcNow < expirationTime)
58 | {
59 | animeList = m_animeListCache[user];
60 | return true;
61 | }
62 | else
63 | {
64 | animeList = null;
65 | return false;
66 | }
67 | }
68 | else
69 | {
70 | animeList = null;
71 | return false;
72 | }
73 | }
74 | finally
75 | {
76 | m_cacheLock.ExitReadLock();
77 | }
78 | }
79 |
80 | public void PutListForUser(string user, MalUserLookupResults animeList)
81 | {
82 | m_cacheLock.EnterWriteLock();
83 |
84 | try
85 | {
86 | if (m_expiration == null)
87 | {
88 | m_animeListCache[user] = animeList;
89 | return;
90 | }
91 |
92 | if (m_cachePutTimesByName.TryGetValue(user, out LinkedListNode> nodeForLastInsert))
93 | {
94 | m_cachePutTimesSortedByTime.Remove(nodeForLastInsert);
95 | }
96 |
97 | DateTime nowUtc = DateTime.UtcNow;
98 | DateTime deleteOlderThan = nowUtc - m_expiration.Value;
99 |
100 | var newNode = m_cachePutTimesSortedByTime.AddFirst(new Tuple(user, nowUtc));
101 |
102 | m_cachePutTimesByName[user] = newNode;
103 |
104 | m_animeListCache[user] = animeList;
105 |
106 | // Check for old entries and remove them
107 |
108 | while (m_cachePutTimesSortedByTime.Count > 0 && m_cachePutTimesSortedByTime.Last.Value.Item2 < deleteOlderThan)
109 | {
110 | string oldUser = m_cachePutTimesSortedByTime.Last.Value.Item1;
111 | m_animeListCache.Remove(oldUser);
112 | m_cachePutTimesByName.Remove(oldUser);
113 | m_cachePutTimesSortedByTime.RemoveLast();
114 | }
115 | }
116 | finally
117 | {
118 | m_cacheLock.ExitWriteLock();
119 | }
120 | }
121 |
122 | public void Dispose()
123 | {
124 | m_cacheLock.Dispose();
125 | }
126 | }
127 | }
128 |
129 | /*
130 | Copyright 2017 Greg Najda
131 |
132 | Licensed under the Apache License, Version 2.0 (the "License");
133 | you may not use this file except in compliance with the License.
134 | You may obtain a copy of the License at
135 |
136 | http://www.apache.org/licenses/LICENSE-2.0
137 |
138 | Unless required by applicable law or agreed to in writing, software
139 | distributed under the License is distributed on an "AS IS" BASIS,
140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
141 | See the License for the specific language governing permissions and
142 | limitations under the License.
143 | */
--------------------------------------------------------------------------------
/MalApi/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("MalApi.UnitTests")]
4 |
--------------------------------------------------------------------------------
/MalApi/CachingMyAnimeListApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace MalApi
9 | {
10 | ///
11 | /// This class is thread-safe if the underlying API is. If the expiration time is null, anime lists are cached for the lifetime of
12 | /// the object. Expired cache entries are only actually removed when a new anime list is inserted into the cache. Cache expiration
13 | /// measurement is susceptible to changes to the system clock.
14 | ///
15 | /// This class only caches user anime lists from GetAnimeListForUser(). Other functions are not cached.
16 | ///
17 | public class CachingMyAnimeListApi : IMyAnimeListApi
18 | {
19 | private IMyAnimeListApi m_underlyingApi;
20 | private bool m_ownUnderlyingApi;
21 | private AnimeListCache m_cache;
22 |
23 | public CachingMyAnimeListApi(IMyAnimeListApi underlyingApi, TimeSpan? expiration, bool ownApi = false)
24 | {
25 | m_underlyingApi = underlyingApi;
26 | m_ownUnderlyingApi = ownApi;
27 | m_cache = new AnimeListCache(expiration);
28 | }
29 |
30 | ///
31 | /// Gets a user's anime list.
32 | ///
33 | ///
34 | ///
35 | ///
36 | public async Task GetAnimeListForUserAsync(string user, CancellationToken cancellationToken)
37 | {
38 | Logging.Log.InfoFormat("Checking cache for user {0}.", user);
39 |
40 | if (m_cache.GetListForUser(user, out MalUserLookupResults cachedAnimeList))
41 | {
42 | if (cachedAnimeList != null)
43 | {
44 | Logging.Log.InfoFormat("Got anime list for {0} from cache.", user);
45 | return cachedAnimeList;
46 | }
47 | else
48 | {
49 | // User does not have an anime list/no such user exists
50 | Logging.Log.InfoFormat("Cache indicates that user {0} does not have an anime list.", user);
51 | throw new MalUserNotFoundException(string.Format("No MAL list exists for {0}.", user));
52 | }
53 | }
54 | else
55 | {
56 | Logging.Log.InfoFormat("Cache did not contain anime list for {0}.", user);
57 |
58 | try
59 | {
60 | MalUserLookupResults animeList = await m_underlyingApi.GetAnimeListForUserAsync(user, cancellationToken)
61 | .ConfigureAwait(continueOnCapturedContext: false);
62 | m_cache.PutListForUser(user, animeList);
63 | return animeList;
64 | }
65 | catch (MalUserNotFoundException)
66 | {
67 | // Cache the fact that the user does not have an anime list
68 | m_cache.PutListForUser(user, null);
69 | throw;
70 | }
71 | }
72 | }
73 |
74 | public Task GetAnimeListForUserAsync(string user)
75 | {
76 | return GetAnimeListForUserAsync(user, CancellationToken.None);
77 | }
78 |
79 | public MalUserLookupResults GetAnimeListForUser(string user)
80 | {
81 | return GetAnimeListForUserAsync(user).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
82 | }
83 |
84 | public Task GetRecentOnlineUsersAsync(CancellationToken cancellationToken)
85 | {
86 | return m_underlyingApi.GetRecentOnlineUsersAsync(cancellationToken);
87 | }
88 |
89 | public Task GetRecentOnlineUsersAsync()
90 | {
91 | return m_underlyingApi.GetRecentOnlineUsersAsync();
92 | }
93 |
94 | public RecentUsersResults GetRecentOnlineUsers()
95 | {
96 | return m_underlyingApi.GetRecentOnlineUsers();
97 | }
98 |
99 | public Task GetAnimeDetailsAsync(int animeId, CancellationToken cancellationToken)
100 | {
101 | return m_underlyingApi.GetAnimeDetailsAsync(animeId, cancellationToken);
102 | }
103 |
104 | public Task GetAnimeDetailsAsync(int animeId)
105 | {
106 | return m_underlyingApi.GetAnimeDetailsAsync(animeId);
107 | }
108 |
109 | public AnimeDetailsResults GetAnimeDetails(int animeId)
110 | {
111 | return m_underlyingApi.GetAnimeDetails(animeId);
112 | }
113 |
114 | public void Dispose()
115 | {
116 | m_cache.Dispose();
117 | if (m_ownUnderlyingApi)
118 | {
119 | m_underlyingApi.Dispose();
120 | }
121 | }
122 | }
123 | }
124 |
125 | /*
126 | Copyright 2017 Greg Najda
127 |
128 | Licensed under the Apache License, Version 2.0 (the "License");
129 | you may not use this file except in compliance with the License.
130 | You may obtain a copy of the License at
131 |
132 | http://www.apache.org/licenses/LICENSE-2.0
133 |
134 | Unless required by applicable law or agreed to in writing, software
135 | distributed under the License is distributed on an "AS IS" BASIS,
136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
137 | See the License for the specific language governing permissions and
138 | limitations under the License.
139 | */
140 |
--------------------------------------------------------------------------------
/MalApi/CompletionStatus.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public enum CompletionStatus
9 | {
10 | Watching = 1,
11 | Completed = 2,
12 | OnHold = 3,
13 | Dropped = 4,
14 | PlanToWatch = 6,
15 | }
16 | }
17 |
18 | /*
19 | Copyright 2012 Greg Najda
20 |
21 | Licensed under the Apache License, Version 2.0 (the "License");
22 | you may not use this file except in compliance with the License.
23 | You may obtain a copy of the License at
24 |
25 | http://www.apache.org/licenses/LICENSE-2.0
26 |
27 | Unless required by applicable law or agreed to in writing, software
28 | distributed under the License is distributed on an "AS IS" BASIS,
29 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30 | See the License for the specific language governing permissions and
31 | limitations under the License.
32 | */
--------------------------------------------------------------------------------
/MalApi/Genre.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public class Genre : IEquatable
9 | {
10 | public int GenreId { get; private set; }
11 | public string Name { get; private set; }
12 |
13 | public Genre(int genreId, string name)
14 | {
15 | GenreId = genreId;
16 | Name = name;
17 | }
18 |
19 | public override bool Equals(object obj)
20 | {
21 | return Equals(obj as Genre);
22 | }
23 |
24 | public bool Equals(Genre other)
25 | {
26 | return other != null && this.GenreId == other.GenreId && this.Name == other.Name;
27 | }
28 |
29 | public override int GetHashCode()
30 | {
31 | unchecked
32 | {
33 | int hash = 23;
34 | hash = hash * 17 + GenreId.GetHashCode();
35 | hash = hash * 17 + Name.GetHashCode();
36 | return hash;
37 | }
38 | }
39 |
40 | public override string ToString()
41 | {
42 | return Name;
43 | }
44 | }
45 | }
46 |
47 | /*
48 | Copyright 2012 Greg Najda
49 |
50 | Licensed under the Apache License, Version 2.0 (the "License");
51 | you may not use this file except in compliance with the License.
52 | You may obtain a copy of the License at
53 |
54 | http://www.apache.org/licenses/LICENSE-2.0
55 |
56 | Unless required by applicable law or agreed to in writing, software
57 | distributed under the License is distributed on an "AS IS" BASIS,
58 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
59 | See the License for the specific language governing permissions and
60 | limitations under the License.
61 | */
--------------------------------------------------------------------------------
/MalApi/IMyAnimeListApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace MalApi
9 | {
10 | public interface IMyAnimeListApi : IDisposable
11 | {
12 | ///
13 | /// Gets a user's anime list.
14 | ///
15 | ///
16 | ///
17 | ///
18 | ///
19 | Task GetAnimeListForUserAsync(string user);
20 |
21 | ///
22 | /// Gets a user's anime list.
23 | ///
24 | ///
25 | ///
26 | ///
27 | ///
28 | ///
29 | Task GetAnimeListForUserAsync(string user, CancellationToken cancellationToken);
30 |
31 | ///
32 | /// Gets a user's anime list.
33 | ///
34 | ///
35 | ///
36 | ///
37 | ///
38 | MalUserLookupResults GetAnimeListForUser(string user);
39 |
40 | ///
41 | /// Gets a list of users that have been on MAL recently.
42 | ///
43 | ///
44 | ///
45 | Task GetRecentOnlineUsersAsync();
46 |
47 | ///
48 | /// Gets a list of users that have been on MAL recently.
49 | ///
50 | ///
51 | ///
52 | ///
53 | Task GetRecentOnlineUsersAsync(CancellationToken cancellationToken);
54 |
55 | ///
56 | /// Gets a list of users that have been on MAL recently.
57 | ///
58 | ///
59 | ///
60 | RecentUsersResults GetRecentOnlineUsers();
61 |
62 | ///
63 | /// Gets information from an anime's "details" page.
64 | ///
65 | ///
66 | ///
67 | ///
68 | Task GetAnimeDetailsAsync(int animeId);
69 |
70 | ///
71 | /// Gets information from an anime's "details" page.
72 | ///
73 | ///
74 | ///
75 | ///
76 | ///
77 | Task GetAnimeDetailsAsync(int animeId, CancellationToken cancellationToken);
78 |
79 | ///
80 | /// Gets information from an anime's "details" page.
81 | ///
82 | ///
83 | ///
84 | ///
85 | AnimeDetailsResults GetAnimeDetails(int animeId);
86 | }
87 | }
88 | /*
89 | Copyright 2017 Greg Najda
90 |
91 | Licensed under the Apache License, Version 2.0 (the "License");
92 | you may not use this file except in compliance with the License.
93 | You may obtain a copy of the License at
94 |
95 | http://www.apache.org/licenses/LICENSE-2.0
96 |
97 | Unless required by applicable law or agreed to in writing, software
98 | distributed under the License is distributed on an "AS IS" BASIS,
99 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
100 | See the License for the specific language governing permissions and
101 | limitations under the License.
102 | */
103 |
--------------------------------------------------------------------------------
/MalApi/Logging.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using Common.Logging;
6 | using Common.Logging.Simple;
7 |
8 | namespace MalApi
9 | {
10 | internal static class Logging
11 | {
12 | internal static ILog Log { get { return Common.Logging.LogManager.GetLogger("MAL API"); } }
13 | }
14 | }
15 |
16 | /*
17 | Copyright 2011 Greg Najda
18 |
19 | Licensed under the Apache License, Version 2.0 (the "License");
20 | you may not use this file except in compliance with the License.
21 | You may obtain a copy of the License at
22 |
23 | http://www.apache.org/licenses/LICENSE-2.0
24 |
25 | Unless required by applicable law or agreed to in writing, software
26 | distributed under the License is distributed on an "AS IS" BASIS,
27 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28 | See the License for the specific language governing permissions and
29 | limitations under the License.
30 | */
--------------------------------------------------------------------------------
/MalApi/MalAnimeInfoFromUserLookup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public class MalAnimeInfoFromUserLookup : IEquatable
9 | {
10 | public int AnimeId { get; private set; }
11 | public string Title { get; private set; }
12 |
13 | ///
14 | /// Could be something other than the enumerated values if MAL adds new types!
15 | ///
16 | public MalAnimeType Type { get; private set; }
17 |
18 | public ICollection Synonyms { get; private set; }
19 | public MalSeriesStatus Status { get; private set; }
20 |
21 | ///
22 | /// Could be 0 for anime that hasn't aired yet or less than the planned number of episodes for a series currently airing.
23 | ///
24 | public int NumEpisodes { get; private set; }
25 |
26 | public UncertainDate StartDate { get; private set; }
27 | public UncertainDate EndDate { get; private set; }
28 | public string ImageUrl { get; private set; }
29 |
30 | public MalAnimeInfoFromUserLookup(int animeId, string title, MalAnimeType type, ICollection synonyms, MalSeriesStatus status,
31 | int numEpisodes, UncertainDate startDate, UncertainDate endDate, string imageUrl)
32 | {
33 | AnimeId = animeId;
34 | Title = title;
35 | Type = type;
36 | Synonyms = synonyms;
37 | Status = status;
38 | NumEpisodes = numEpisodes;
39 | StartDate = startDate;
40 | EndDate = endDate;
41 | ImageUrl = imageUrl;
42 | }
43 |
44 | public override bool Equals(object obj)
45 | {
46 | return Equals(obj as MalAnimeInfoFromUserLookup);
47 | }
48 |
49 | public bool Equals(MalAnimeInfoFromUserLookup other)
50 | {
51 | if (other == null) return false;
52 | return this.AnimeId == other.AnimeId;
53 | }
54 |
55 | public override int GetHashCode()
56 | {
57 | return AnimeId.GetHashCode();
58 | }
59 |
60 | public override string ToString()
61 | {
62 | return Title;
63 | }
64 | }
65 | }
66 |
67 | /*
68 | Copyright 2012 Greg Najda
69 |
70 | Licensed under the Apache License, Version 2.0 (the "License");
71 | you may not use this file except in compliance with the License.
72 | You may obtain a copy of the License at
73 |
74 | http://www.apache.org/licenses/LICENSE-2.0
75 |
76 | Unless required by applicable law or agreed to in writing, software
77 | distributed under the License is distributed on an "AS IS" BASIS,
78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
79 | See the License for the specific language governing permissions and
80 | limitations under the License.
81 | */
--------------------------------------------------------------------------------
/MalApi/MalAnimeNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | ///
9 | /// Indicates that the anime that was searched for does not exist.
10 | ///
11 | public class MalAnimeNotFoundException : MalApiException
12 | {
13 | public MalAnimeNotFoundException() { }
14 | public MalAnimeNotFoundException(string message) : base(message) { }
15 | public MalAnimeNotFoundException(string message, Exception inner) : base(message, inner) { }
16 | }
17 | }
18 |
19 | /*
20 | Copyright 2017 Greg Najda
21 |
22 | Licensed under the Apache License, Version 2.0 (the "License");
23 | you may not use this file except in compliance with the License.
24 | You may obtain a copy of the License at
25 |
26 | http://www.apache.org/licenses/LICENSE-2.0
27 |
28 | Unless required by applicable law or agreed to in writing, software
29 | distributed under the License is distributed on an "AS IS" BASIS,
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 | See the License for the specific language governing permissions and
32 | limitations under the License.
33 | */
--------------------------------------------------------------------------------
/MalApi/MalAnimeType.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public enum MalAnimeType
9 | {
10 | ///
11 | /// The type has not been entered in MAL's database yet.
12 | ///
13 | Unknown = 0,
14 | Tv = 1,
15 | Ova = 2,
16 | Movie = 3,
17 | Special = 4,
18 | Ona = 5,
19 | Music = 6
20 | }
21 | }
22 |
23 | /*
24 | Copyright 2012 Greg Najda
25 |
26 | Licensed under the Apache License, Version 2.0 (the "License");
27 | you may not use this file except in compliance with the License.
28 | You may obtain a copy of the License at
29 |
30 | http://www.apache.org/licenses/LICENSE-2.0
31 |
32 | Unless required by applicable law or agreed to in writing, software
33 | distributed under the License is distributed on an "AS IS" BASIS,
34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
35 | See the License for the specific language governing permissions and
36 | limitations under the License.
37 | */
--------------------------------------------------------------------------------
/MalApi/MalApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net45;netstandard1.3
5 | MalApi
6 | MalApi
7 | 4.0.0-Beta3
8 | Greg Najda
9 |
10 |
11 | Fixed a bug where cancelling when using an async version of a function would throw MalApiException instead of TaskCanceledException. This is especially a problem when using a RetryOnFailureMyAnimeListApi because that would cause it to retry after you told it to cancel.
12 | http://www.apache.org/licenses/LICENSE-2.0.html
13 | https://github.com/LHCGreg/mal-api
14 | anime myanimelist myanimelist.net MAL
15 | MalApi
16 |
17 |
18 |
19 |
20 |
22 | false
23 |
24 | MalApi is a .NET library for accessing the myanimelist.net API. You can get a user's anime list, get a list of recently online users, and get an anime's genres. You must set the UserAgent property of the MyAnimeListApi object to your myanimelist.net API key.
25 |
26 |
27 | false
28 |
29 |
30 | false
31 |
32 |
33 |
34 | 1591
35 | bin\Release\$(TargetFramework)\MalApi.xml
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/MalApi/MalApiException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | ///
9 | /// Generic exception for errors
10 | ///
11 | public class MalApiException : Exception
12 | {
13 | public MalApiException() { }
14 | public MalApiException(string message) : base(message) { }
15 | public MalApiException(string message, Exception inner) : base(message, inner) { }
16 | }
17 | }
18 |
19 | /*
20 | Copyright 2017 Greg Najda
21 |
22 | Licensed under the Apache License, Version 2.0 (the "License");
23 | you may not use this file except in compliance with the License.
24 | You may obtain a copy of the License at
25 |
26 | http://www.apache.org/licenses/LICENSE-2.0
27 |
28 | Unless required by applicable law or agreed to in writing, software
29 | distributed under the License is distributed on an "AS IS" BASIS,
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 | See the License for the specific language governing permissions and
32 | limitations under the License.
33 | */
--------------------------------------------------------------------------------
/MalApi/MalApiRequestException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | ///
9 | /// Thrown when there is an error communicating with MAL.
10 | ///
11 | public class MalApiRequestException : MalApiException
12 | {
13 | public MalApiRequestException() { }
14 | public MalApiRequestException(string message) : base(message) { }
15 | public MalApiRequestException(string message, Exception inner) : base(message, inner) { }
16 | }
17 | }
18 |
19 | /*
20 | Copyright 2017 Greg Najda
21 |
22 | Licensed under the Apache License, Version 2.0 (the "License");
23 | you may not use this file except in compliance with the License.
24 | You may obtain a copy of the License at
25 |
26 | http://www.apache.org/licenses/LICENSE-2.0
27 |
28 | Unless required by applicable law or agreed to in writing, software
29 | distributed under the License is distributed on an "AS IS" BASIS,
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 | See the License for the specific language governing permissions and
32 | limitations under the License.
33 | */
--------------------------------------------------------------------------------
/MalApi/MalAppInfoXml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.IO;
6 | using System.Xml.Linq;
7 | using System.Text.RegularExpressions;
8 | using System.Threading.Tasks;
9 | using System.Threading;
10 |
11 | namespace MalApi
12 | {
13 | public static class MalAppInfoXml
14 | {
15 | ///
16 | /// Parses XML obtained from malappinfo.php. The XML is sanitized to account for MAL's invalid XML if, for example,
17 | /// a user has a & character in their tags.
18 | ///
19 | ///
20 | ///
21 | ///
22 | ///
23 | ///
24 | public static async Task ParseAsync(TextReader xmlTextReader, CancellationToken cancellationToken)
25 | {
26 | Logging.Log.Trace("Sanitizing XML.");
27 | using (StringReader sanitizedXmlTextReader = await SanitizeAnimeListXmlAsync(xmlTextReader, cancellationToken).ConfigureAwait(continueOnCapturedContext: false))
28 | {
29 | Logging.Log.Trace("XML sanitized.");
30 |
31 | // No async version of XDocument.Load in the full framework yet.
32 | // StringReader won't block though.
33 | XDocument doc = XDocument.Load(sanitizedXmlTextReader);
34 | return Parse(doc);
35 | }
36 | }
37 |
38 | ///
39 | /// Parses XML obtained from malappinfo.php. The XML is sanitized to account for MAL's invalid XML if, for example,
40 | /// a user has a & character in their tags.
41 | ///
42 | ///
43 | ///
44 | ///
45 | ///
46 | public static Task ParseAsync(TextReader xmlTextReader)
47 | {
48 | return ParseAsync(xmlTextReader, CancellationToken.None);
49 | }
50 |
51 | ///
52 | /// Parses XML obtained from malappinfo.php. The XML is sanitized to account for MAL's invalid XML if, for example,
53 | /// a user has a & character in their tags.
54 | ///
55 | ///
56 | ///
57 | ///
58 | ///
59 | public static MalUserLookupResults Parse(TextReader xmlTextReader)
60 | {
61 | return ParseAsync(xmlTextReader).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
62 | }
63 |
64 | // Rumor has it that compiled regexes are far more performant than non-compiled regexes on large pieces of text.
65 | // I haven't profiled it though.
66 | private static Lazy s_tagElementContentsRegex =
67 | new Lazy(() => new Regex("(?.*?)", RegexOptions.Compiled | RegexOptions.CultureInvariant));
68 | private static Regex TagElementContentsRegex { get { return s_tagElementContentsRegex.Value; } }
69 |
70 | private static Lazy s_nonEntityAmpersandRegex =
71 | new Lazy(() => new Regex("&(?!lt;)(?!gt;)(?!amp;)(?!apos;)(?!quot;)(?!#x[0-9a-fA-f]+;)(?!#[0-9]+;)", RegexOptions.Compiled | RegexOptions.CultureInvariant));
72 | private static Regex NonEntityAmpersandRegex { get { return s_nonEntityAmpersandRegex.Value; } }
73 |
74 | // Remove any code points not in: U+0009, U+000A, U+000D, U+0020–U+D7FF, U+E000–U+FFFD (see http://en.wikipedia.org/wiki/Xml)
75 | private static Lazy s_invalidXmlCharacterRegex =
76 | new Lazy(() => new Regex("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]", RegexOptions.Compiled | RegexOptions.CultureInvariant));
77 | private static Regex InvalidXmlCharacterRegex { get { return s_invalidXmlCharacterRegex.Value; } }
78 |
79 | // Replace & with & only if the & is not part of < > & ' " ; ;
80 | private static MatchEvaluator TagElementContentsReplacer = (Match match) =>
81 | {
82 | string tagText = match.Groups["TagText"].Value;
83 | string replacementTagText = NonEntityAmpersandRegex.Replace(tagText, "&");
84 | replacementTagText = InvalidXmlCharacterRegex.Replace(replacementTagText, "");
85 | return "" + replacementTagText + "";
86 | };
87 |
88 | ///
89 | /// Sanitizes anime list XML which is not always well-formed. If a user uses & characters in their tags,
90 | /// they will not be escaped in the XML.
91 | ///
92 | ///
93 | ///
94 | ///
95 | private static async Task SanitizeAnimeListXmlAsync(TextReader xmlTextReader, CancellationToken cancellationToken)
96 | {
97 | string rawXml = await xmlTextReader.ReadToEndAsync().ConfigureAwait(continueOnCapturedContext: false);
98 | string sanitizedXml = TagElementContentsRegex.Replace(rawXml, TagElementContentsReplacer);
99 | return new StringReader(sanitizedXml);
100 | }
101 |
102 | ///
103 | /// Parses XML obtained from malappinfo.php.
104 | ///
105 | ///
106 | ///
107 | public static MalUserLookupResults Parse(XDocument doc)
108 | {
109 | Logging.Log.Trace("Parsing XML.");
110 |
111 | XElement error = doc.Root.Element("error");
112 | if (error != null && (string)error == "Invalid username")
113 | {
114 | throw new MalUserNotFoundException("No MAL list exists for this user.");
115 | }
116 | else if (error != null)
117 | {
118 | throw new MalApiException((string)error);
119 | }
120 |
121 | if (!doc.Root.HasElements)
122 | {
123 | throw new MalUserNotFoundException("No MAL list exists for this user.");
124 | }
125 |
126 | XElement myinfo = GetExpectedElement(doc.Root, "myinfo");
127 | int userId = GetElementValueInt(myinfo, "user_id");
128 | string canonicalUserName = GetElementValueString(myinfo, "user_name");
129 |
130 | List entries = new List();
131 |
132 | IEnumerable animes = doc.Root.Elements("anime");
133 | foreach (XElement anime in animes)
134 | {
135 | int animeId = GetElementValueInt(anime, "series_animedb_id");
136 | string title = GetElementValueString(anime, "series_title");
137 |
138 | string synonymList = GetElementValueString(anime, "series_synonyms");
139 | string[] rawSynonyms = synonymList.Split(SynonymSeparator, StringSplitOptions.RemoveEmptyEntries);
140 |
141 | // filter out synonyms that are the same as the main title
142 | HashSet synonyms = new HashSet(rawSynonyms.Where(synonym => !synonym.Equals(title, StringComparison.Ordinal)));
143 |
144 | int seriesTypeInt = GetElementValueInt(anime, "series_type");
145 | MalAnimeType seriesType = (MalAnimeType)seriesTypeInt;
146 |
147 | int numEpisodes = GetElementValueInt(anime, "series_episodes");
148 |
149 | int seriesStatusInt = GetElementValueInt(anime, "series_status");
150 | MalSeriesStatus seriesStatus = (MalSeriesStatus)seriesStatusInt;
151 |
152 | string seriesStartString = GetElementValueString(anime, "series_start");
153 | UncertainDate seriesStart = UncertainDate.FromMalDateString(seriesStartString);
154 |
155 | string seriesEndString = GetElementValueString(anime, "series_end");
156 | UncertainDate seriesEnd = UncertainDate.FromMalDateString(seriesEndString);
157 |
158 | string seriesImage = GetElementValueString(anime, "series_image");
159 |
160 | MalAnimeInfoFromUserLookup animeInfo = new MalAnimeInfoFromUserLookup(animeId: animeId, title: title,
161 | type: seriesType, synonyms: synonyms, status: seriesStatus, numEpisodes: numEpisodes, startDate: seriesStart,
162 | endDate: seriesEnd, imageUrl: seriesImage);
163 |
164 |
165 | int numEpisodesWatched = GetElementValueInt(anime, "my_watched_episodes");
166 |
167 | string myStartDateString = GetElementValueString(anime, "my_start_date");
168 | UncertainDate myStartDate = UncertainDate.FromMalDateString(myStartDateString);
169 |
170 | string myFinishDateString = GetElementValueString(anime, "my_finish_date");
171 | UncertainDate myFinishDate = UncertainDate.FromMalDateString(myFinishDateString);
172 |
173 | decimal rawScore = GetElementValueDecimal(anime, "my_score");
174 | decimal? myScore = rawScore == 0 ? (decimal?)null : rawScore;
175 |
176 | int completionStatusInt = GetElementValueInt(anime, "my_status");
177 | CompletionStatus completionStatus = (CompletionStatus)completionStatusInt;
178 |
179 | long lastUpdatedUnixTimestamp = GetElementValueLong(anime, "my_last_updated");
180 | DateTime lastUpdated = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + TimeSpan.FromSeconds(lastUpdatedUnixTimestamp);
181 |
182 | string rawTagsString = GetElementValueString(anime, "my_tags");
183 | string[] untrimmedTags = rawTagsString.Split(TagSeparator, StringSplitOptions.RemoveEmptyEntries);
184 | List tags = new List(untrimmedTags.Select(tag => tag.Trim()));
185 |
186 | MyAnimeListEntry entry = new MyAnimeListEntry(score: myScore, status: completionStatus, numEpisodesWatched: numEpisodesWatched,
187 | myStartDate: myStartDate, myFinishDate: myFinishDate, myLastUpdate: lastUpdated, animeInfo: animeInfo, tags: tags);
188 |
189 | entries.Add(entry);
190 | }
191 |
192 | MalUserLookupResults results = new MalUserLookupResults(userId: userId, canonicalUserName: canonicalUserName, animeList: entries);
193 | Logging.Log.Trace("Parsed XML.");
194 | return results;
195 | }
196 |
197 | private static readonly string[] SynonymSeparator = new string[] { "; " };
198 | private static readonly char[] TagSeparator = new char[] { ',' };
199 |
200 | private static XElement GetExpectedElement(XContainer container, string elementName)
201 | {
202 | XElement element = container.Element(elementName);
203 | if (element == null)
204 | {
205 | throw new MalApiException(string.Format("Did not find element {0}.", elementName));
206 | }
207 | return element;
208 | }
209 |
210 | private static string GetElementValueString(XContainer container, string elementName)
211 | {
212 | XElement element = GetExpectedElement(container, elementName);
213 |
214 | try
215 | {
216 | return (string)element;
217 | }
218 | catch (FormatException ex)
219 | {
220 | throw new MalApiException(string.Format("Unexpected value \"{0}\" for element {1}.", element.Value, elementName), ex);
221 | }
222 | }
223 |
224 | private static int GetElementValueInt(XContainer container, string elementName)
225 | {
226 | XElement element = GetExpectedElement(container, elementName);
227 |
228 | try
229 | {
230 | return (int)element;
231 | }
232 | catch (FormatException ex)
233 | {
234 | throw new MalApiException(string.Format("Unexpected value \"{0}\" for element {1}.", element.Value, elementName), ex);
235 | }
236 | }
237 |
238 | private static long GetElementValueLong(XContainer container, string elementName)
239 | {
240 | XElement element = GetExpectedElement(container, elementName);
241 |
242 | try
243 | {
244 | return (long)element;
245 | }
246 | catch (FormatException ex)
247 | {
248 | throw new MalApiException(string.Format("Unexpected value \"{0}\" for element {1}.", element.Value, elementName), ex);
249 | }
250 | }
251 |
252 | private static decimal GetElementValueDecimal(XContainer container, string elementName)
253 | {
254 | XElement element = GetExpectedElement(container, elementName);
255 |
256 | try
257 | {
258 | return (decimal)element;
259 | }
260 | catch (FormatException ex)
261 | {
262 | throw new MalApiException(string.Format("Unexpected value \"{0}\" for element {1}.", element.Value, elementName), ex);
263 | }
264 | }
265 | }
266 | }
267 |
268 | /*
269 | Copyright 2017 Greg Najda
270 |
271 | Licensed under the Apache License, Version 2.0 (the "License");
272 | you may not use this file except in compliance with the License.
273 | You may obtain a copy of the License at
274 |
275 | http://www.apache.org/licenses/LICENSE-2.0
276 |
277 | Unless required by applicable law or agreed to in writing, software
278 | distributed under the License is distributed on an "AS IS" BASIS,
279 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
280 | See the License for the specific language governing permissions and
281 | limitations under the License.
282 | */
283 |
--------------------------------------------------------------------------------
/MalApi/MalSeriesStatus.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public enum MalSeriesStatus
9 | {
10 | Airing = 1,
11 | FinishedAiring = 2,
12 | NotYetAired = 3
13 | }
14 | }
15 |
16 | /*
17 | Copyright 2012 Greg Najda
18 |
19 | Licensed under the Apache License, Version 2.0 (the "License");
20 | you may not use this file except in compliance with the License.
21 | You may obtain a copy of the License at
22 |
23 | http://www.apache.org/licenses/LICENSE-2.0
24 |
25 | Unless required by applicable law or agreed to in writing, software
26 | distributed under the License is distributed on an "AS IS" BASIS,
27 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28 | See the License for the specific language governing permissions and
29 | limitations under the License.
30 | */
--------------------------------------------------------------------------------
/MalApi/MalUserLookupResults.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public class MalUserLookupResults
9 | {
10 | public ICollection AnimeList { get; private set; }
11 | public int UserId { get; private set; }
12 |
13 | ///
14 | /// The user name as it appears in MAL. This might differ in capitalization from the username that was searched for.
15 | ///
16 | public string CanonicalUserName { get; private set; }
17 |
18 | public MalUserLookupResults(int userId, string canonicalUserName, ICollection animeList)
19 | {
20 | UserId = userId;
21 | CanonicalUserName = canonicalUserName;
22 | AnimeList = animeList;
23 | }
24 | }
25 | }
26 |
27 | /*
28 | Copyright 2012 Greg Najda
29 |
30 | Licensed under the Apache License, Version 2.0 (the "License");
31 | you may not use this file except in compliance with the License.
32 | You may obtain a copy of the License at
33 |
34 | http://www.apache.org/licenses/LICENSE-2.0
35 |
36 | Unless required by applicable law or agreed to in writing, software
37 | distributed under the License is distributed on an "AS IS" BASIS,
38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
39 | See the License for the specific language governing permissions and
40 | limitations under the License.
41 | */
--------------------------------------------------------------------------------
/MalApi/MalUserNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | ///
9 | /// Indicates that the user that was searched for does not exist.
10 | ///
11 | public class MalUserNotFoundException : MalApiException
12 | {
13 | public MalUserNotFoundException() { }
14 | public MalUserNotFoundException(string message) : base(message) { }
15 | public MalUserNotFoundException(string message, Exception inner) : base(message, inner) { }
16 | }
17 | }
18 |
19 | /*
20 | Copyright 2017 Greg Najda
21 |
22 | Licensed under the Apache License, Version 2.0 (the "License");
23 | you may not use this file except in compliance with the License.
24 | You may obtain a copy of the License at
25 |
26 | http://www.apache.org/licenses/LICENSE-2.0
27 |
28 | Unless required by applicable law or agreed to in writing, software
29 | distributed under the License is distributed on an "AS IS" BASIS,
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 | See the License for the specific language governing permissions and
32 | limitations under the License.
33 | */
--------------------------------------------------------------------------------
/MalApi/MyAnimeListApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Net;
6 | using System.IO;
7 | using System.Text.RegularExpressions;
8 | using System.Net.Http;
9 | using System.Threading.Tasks;
10 | using System.Threading;
11 |
12 | namespace MalApi
13 | {
14 | ///
15 | /// Class for accessing myanimelist.net. Methods are thread-safe. Properties are not.
16 | ///
17 | public class MyAnimeListApi : IMyAnimeListApi
18 | {
19 | private const string MalAppInfoUri = "https://myanimelist.net/malappinfo.php?status=all&type=anime";
20 | private const string RecentOnlineUsersUri = "https://myanimelist.net/users.php";
21 |
22 | ///
23 | /// What to set the user agent http header to in API requests. Null to use the default .NET user agent.
24 | /// This is current synonymous with the MalApiKey property because that is how API keys
25 | /// are passed to MAL.
26 | ///
27 | public string UserAgent { get; set; }
28 |
29 | ///
30 | /// MAL API key to use. Some methods require this to be set or else MAL will return an error.
31 | /// The documentation for each method states whether or not this is required.
32 | /// This is currently synonymous with the UserAgent property because that is how
33 | /// API keys are passed to MAL.
34 | ///
35 | public string MalApiKey { get { return UserAgent; } set { UserAgent = value; } }
36 |
37 | ///
38 | /// Timeout in milliseconds for requests to MAL. Defaults to 15000 (15s).
39 | ///
40 | public int TimeoutInMs
41 | {
42 | get { return m_httpClient.Timeout.Milliseconds; }
43 | set { m_httpClient.Timeout = TimeSpan.FromMilliseconds(value); }
44 | }
45 |
46 | private HttpClientHandler m_httpHandler;
47 | private HttpClient m_httpClient;
48 |
49 | public MyAnimeListApi()
50 | {
51 | m_httpHandler = new HttpClientHandler()
52 | {
53 | AllowAutoRedirect = true,
54 | UseCookies = false,
55 |
56 | // Very important optimization! Time to get an anime list of ~150 entries 2.6s -> 0.7s
57 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
58 | };
59 | m_httpClient = new HttpClient(m_httpHandler);
60 | TimeoutInMs = 15 * 1000;
61 | }
62 |
63 | private HttpRequestMessage InitNewRequest(string uri, HttpMethod method)
64 | {
65 | HttpRequestMessage request = new HttpRequestMessage(method, uri);
66 |
67 | if (UserAgent != null)
68 | {
69 | request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
70 | }
71 |
72 | return request;
73 | }
74 |
75 | private Task ProcessRequestAsync(HttpRequestMessage request, Func processingFunc, string baseErrorMessage, CancellationToken cancellationToken)
76 | {
77 | return ProcessRequestAsync(request, (string html, object dummy) => processingFunc(html), (object)null,
78 | httpErrorStatusHandler: null, baseErrorMessage: baseErrorMessage, cancellationToken: cancellationToken);
79 | }
80 |
81 | ///
82 | /// Expected to do one of the following:
83 | /// 1) Set handled to true and return a valid result
84 | /// 2) Throw a new exception that better matches the abstraction level, for example a MalAnimeNotFoundException.
85 | /// 3) Set handled to false and return anything
86 | ///
87 | ///
88 | ///
89 | ///
90 | ///
91 | ///
92 | ///
93 | delegate TReturn HttpErrorStatusHandler(HttpResponseMessage response, TData data, out bool handled);
94 |
95 | private async Task ProcessRequestAsync(HttpRequestMessage request, Func processingFunc, TData data, HttpErrorStatusHandler httpErrorStatusHandler, string baseErrorMessage, CancellationToken cancellationToken)
96 | {
97 | string responseBody = null;
98 | try
99 | {
100 | Logging.Log.DebugFormat("Starting MAL request to {0}", request.RequestUri);
101 |
102 | // Need to read the entire content at once here because response.Content.ReadAsStringAsync doesn't support cancellation.
103 | using (HttpResponseMessage response = await m_httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false))
104 | {
105 | Logging.Log.DebugFormat("Got response. Status code = {0}.", (int)response.StatusCode);
106 |
107 | if (response.StatusCode == HttpStatusCode.OK)
108 | {
109 | responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);
110 | Logging.Log.Debug("Read response body.");
111 | return processingFunc(responseBody, data);
112 | }
113 | else
114 | {
115 | if (httpErrorStatusHandler != null)
116 | {
117 | TReturn result = httpErrorStatusHandler(response, data, out bool handled);
118 |
119 | // If the handler knew what to do and returned a result, return that result
120 | if (handled)
121 | {
122 | return result;
123 | }
124 |
125 | // If the handler knew what to do and threw a better exception, it will get caught below and thrown further.
126 | }
127 |
128 | // If there was a handler and it did not know what to do with the http error,
129 | // or if no handler was passed, throw an exception.
130 | throw new MalApiRequestException(string.Format("{0} Status code was {1}", baseErrorMessage, (int)response.StatusCode));
131 | }
132 | }
133 | }
134 | catch (MalUserNotFoundException)
135 | {
136 | throw;
137 | }
138 | catch (MalAnimeNotFoundException)
139 | {
140 | throw;
141 | }
142 | catch (OperationCanceledException)
143 | {
144 | throw;
145 | }
146 | catch (MalApiException)
147 | {
148 | // Log the body of the response returned by the API server if there was an error.
149 | // Don't log it otherwise, logs could get big then.
150 | if (responseBody != null)
151 | {
152 | Logging.Log.DebugFormat("Response body:{0}{1}", Environment.NewLine, responseBody);
153 | }
154 | throw;
155 | }
156 | catch (Exception ex)
157 | {
158 | if (responseBody != null)
159 | {
160 | // Since we read the response, the error was in processing the response, not with doing the request/response.
161 | Logging.Log.DebugFormat("Response body:{0}{1}", Environment.NewLine, responseBody);
162 | throw new MalApiException(string.Format("{0} {1}", baseErrorMessage, ex.Message), ex);
163 | }
164 | else
165 | {
166 | // If we didn't read a response, then there was an error with the request/response that may be fixable with a retry.
167 | throw new MalApiRequestException(string.Format("{0} {1}", baseErrorMessage, ex.Message), ex);
168 | }
169 | }
170 | }
171 |
172 | ///
173 | /// Gets a user's anime list. This method requires a MAL API key.
174 | ///
175 | ///
176 | ///
177 | ///
178 | ///
179 | public Task GetAnimeListForUserAsync(string user)
180 | {
181 | return GetAnimeListForUserAsync(user, CancellationToken.None);
182 | }
183 |
184 | ///
185 | /// Gets a user's anime list. This method requires a MAL API key.
186 | ///
187 | ///
188 | ///
189 | ///
190 | ///
191 | ///
192 | public async Task GetAnimeListForUserAsync(string user, CancellationToken cancellationToken)
193 | {
194 | string userInfoUri = MalAppInfoUri + "&u=" + Uri.EscapeDataString(user);
195 |
196 | Logging.Log.InfoFormat("Getting anime list for MAL user {0} using URI {1}", user, userInfoUri);
197 |
198 | Func responseProcessingFunc = (xml) =>
199 | {
200 | using (TextReader xmlTextReader = new StringReader(xml))
201 | {
202 | try
203 | {
204 | return MalAppInfoXml.Parse(xmlTextReader);
205 | }
206 | catch (MalUserNotFoundException ex)
207 | {
208 | throw new MalUserNotFoundException(string.Format("No MAL list exists for {0}.", user), ex);
209 | }
210 | }
211 | };
212 |
213 | try
214 | {
215 | HttpRequestMessage request = InitNewRequest(userInfoUri, HttpMethod.Get);
216 | MalUserLookupResults parsedList = await ProcessRequestAsync(request, responseProcessingFunc, cancellationToken: cancellationToken,
217 | baseErrorMessage: string.Format("Failed getting anime list for user {0} using url {1}", user, userInfoUri)).ConfigureAwait(continueOnCapturedContext: false);
218 |
219 | Logging.Log.InfoFormat("Successfully retrieved anime list for user {0}", user);
220 | return parsedList;
221 | }
222 | catch (OperationCanceledException)
223 | {
224 | Logging.Log.InfoFormat("Canceled getting anime list for MAL user {0}", user);
225 | throw;
226 | }
227 | }
228 |
229 | ///
230 | /// Gets a user's anime list. This method requires a MAL API key.
231 | ///
232 | ///
233 | ///
234 | ///
235 | ///
236 | public MalUserLookupResults GetAnimeListForUser(string user)
237 | {
238 | return GetAnimeListForUserAsync(user).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
239 | }
240 |
241 | private static Lazy s_recentOnlineUsersRegex =
242 | new Lazy(() => new Regex("/profile/(?[^\"]+)\">\\k",
243 | RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase));
244 | public static Regex RecentOnlineUsersRegex { get { return s_recentOnlineUsersRegex.Value; } }
245 |
246 | public Task GetRecentOnlineUsersAsync()
247 | {
248 | return GetRecentOnlineUsersAsync(CancellationToken.None);
249 | }
250 |
251 | ///
252 | /// Gets a list of users that have been on MAL recently. This scrapes the HTML on the recent users page and therefore
253 | /// can break if MAL changes the HTML on that page. This method does not require a MAL API key.
254 | ///
255 | ///
256 | ///
257 | ///
258 | public async Task GetRecentOnlineUsersAsync(CancellationToken cancellationToken)
259 | {
260 | Logging.Log.InfoFormat("Getting list of recent online MAL users using URI {0}", RecentOnlineUsersUri);
261 |
262 | HttpRequestMessage request = InitNewRequest(RecentOnlineUsersUri, HttpMethod.Get);
263 |
264 | try
265 | {
266 | RecentUsersResults recentUsers = await ProcessRequestAsync(request, ScrapeUsersFromHtml,
267 | baseErrorMessage: "Failed getting list of recent MAL users.", cancellationToken: cancellationToken)
268 | .ConfigureAwait(continueOnCapturedContext: false);
269 |
270 | Logging.Log.Info("Successfully got list of recent online MAL users.");
271 | return recentUsers;
272 | }
273 | catch (OperationCanceledException)
274 | {
275 | Logging.Log.Info("Canceled getting list of recent online MAL users.");
276 | throw;
277 | }
278 | }
279 |
280 | ///
281 | /// Gets a list of users that have been on MAL recently. This scrapes the HTML on the recent users page and therefore
282 | /// can break if MAL changes the HTML on that page. This method does not require a MAL API key.
283 | ///
284 | ///
285 | ///
286 | public RecentUsersResults GetRecentOnlineUsers()
287 | {
288 | return GetRecentOnlineUsersAsync().ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
289 | }
290 |
291 | private RecentUsersResults ScrapeUsersFromHtml(string recentUsersHtml)
292 | {
293 | List users = new List();
294 | MatchCollection userMatches = RecentOnlineUsersRegex.Matches(recentUsersHtml);
295 | foreach (Match userMatch in userMatches)
296 | {
297 | string username = userMatch.Groups["Username"].ToString();
298 | users.Add(username);
299 | }
300 |
301 | if (users.Count == 0)
302 | {
303 | throw new MalApiException("0 users found in recent users page html.");
304 | }
305 |
306 | return new RecentUsersResults(users);
307 | }
308 |
309 | private static readonly string AnimeDetailsUrlFormat = "https://myanimelist.net/anime/{0}";
310 | private static Lazy s_animeDetailsRegex = new Lazy(() => new Regex(
311 | @"Genres:\s*?(?:\d+)/[^""]+?""[^>]*?>(?.*?)(?:, )?)*",
312 | RegexOptions.Compiled));
313 | private static Regex AnimeDetailsRegex { get { return s_animeDetailsRegex.Value; } }
314 |
315 | ///
316 | /// Gets information from an anime's "details" page. This method uses HTML scraping and so may break if MAL changes the HTML.
317 | /// This method does not require a MAL API key.
318 | ///
319 | ///
320 | ///
321 | ///
322 | public Task GetAnimeDetailsAsync(int animeId)
323 | {
324 | return GetAnimeDetailsAsync(animeId, CancellationToken.None);
325 | }
326 |
327 | ///
328 | /// Gets information from an anime's "details" page. This method uses HTML scraping and so may break if MAL changes the HTML.
329 | /// This method does not require a MAL API key.
330 | ///
331 | ///
332 | ///
333 | ///
334 | ///
335 | public async Task GetAnimeDetailsAsync(int animeId, CancellationToken cancellationToken)
336 | {
337 | string url = string.Format(AnimeDetailsUrlFormat, animeId);
338 | Logging.Log.InfoFormat("Getting anime details for anime ID {0} from {1}.", animeId, url);
339 |
340 | HttpRequestMessage request = InitNewRequest(url, HttpMethod.Get);
341 |
342 | try
343 | {
344 | AnimeDetailsResults results = await ProcessRequestAsync(request, ScrapeAnimeDetailsFromHtml, animeId,
345 | httpErrorStatusHandler: GetAnimeDetailsHttpErrorStatusHandler, cancellationToken: cancellationToken,
346 | baseErrorMessage: string.Format("Failed getting anime details for anime ID {0}.", animeId))
347 | .ConfigureAwait(continueOnCapturedContext: false);
348 | Logging.Log.InfoFormat("Successfully got details from {0}.", url);
349 | return results;
350 | }
351 | catch (OperationCanceledException)
352 | {
353 | Logging.Log.InfoFormat("Canceled getting anime details for anime ID {0}.", animeId);
354 | throw;
355 | }
356 | }
357 |
358 | ///
359 | /// Gets information from an anime's "details" page. This method uses HTML scraping and so may break if MAL changes the HTML.
360 | /// This method does not require a MAL API key.
361 | ///
362 | ///
363 | ///
364 | ///
365 | public AnimeDetailsResults GetAnimeDetails(int animeId)
366 | {
367 | return GetAnimeDetailsAsync(animeId).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
368 | }
369 |
370 | // If getting anime details page returned a 404, throw a MalAnimeNotFound exception instead of letting
371 | // a generic MalApiException be thrown.
372 | private static AnimeDetailsResults GetAnimeDetailsHttpErrorStatusHandler(HttpResponseMessage response, int animeId, out bool handled)
373 | {
374 | if (response.StatusCode == HttpStatusCode.NotFound)
375 | {
376 | throw new MalAnimeNotFoundException(string.Format("No anime with id {0} exists.", animeId));
377 | }
378 | else
379 | {
380 | handled = false;
381 | return null;
382 | }
383 | }
384 |
385 | // internal for unit testing
386 | internal AnimeDetailsResults ScrapeAnimeDetailsFromHtml(string animeDetailsHtml, int animeId)
387 | {
388 | Match match = AnimeDetailsRegex.Match(animeDetailsHtml);
389 |
390 | if (!match.Success)
391 | {
392 | throw new MalApiException(string.Format("Could not extract information from {0}.", string.Format(AnimeDetailsUrlFormat, animeId)));
393 | }
394 |
395 | Group genreIds = match.Groups["GenreId"];
396 | Group genreNames = match.Groups["GenreName"];
397 | List genres = new List();
398 | for (int i = 0; i < genreIds.Captures.Count; i++)
399 | {
400 | string genreIdString = genreIds.Captures[i].Value;
401 | int genreId = int.Parse(genreIdString);
402 | string genreName = WebUtility.HtmlDecode(genreNames.Captures[i].Value);
403 | genres.Add(new Genre(genreId, genreName));
404 | }
405 |
406 | return new AnimeDetailsResults(genres);
407 | }
408 |
409 | public void Dispose()
410 | {
411 | m_httpClient.Dispose();
412 | m_httpHandler.Dispose();
413 | }
414 | }
415 | }
416 |
417 | /*
418 | Copyright 2017 Greg Najda
419 |
420 | Licensed under the Apache License, Version 2.0 (the "License");
421 | you may not use this file except in compliance with the License.
422 | You may obtain a copy of the License at
423 |
424 | http://www.apache.org/licenses/LICENSE-2.0
425 |
426 | Unless required by applicable law or agreed to in writing, software
427 | distributed under the License is distributed on an "AS IS" BASIS,
428 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
429 | See the License for the specific language governing permissions and
430 | limitations under the License.
431 | */
432 |
--------------------------------------------------------------------------------
/MalApi/MyAnimeListEntry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public class MyAnimeListEntry : IEquatable
9 | {
10 | public decimal? Score { get; private set; }
11 | public CompletionStatus Status { get; private set; }
12 | public int NumEpisodesWatched { get; private set; }
13 | public UncertainDate MyStartDate { get; private set; }
14 | public UncertainDate MyFinishDate { get; private set; }
15 | public DateTime MyLastUpdate { get; private set; }
16 | public MalAnimeInfoFromUserLookup AnimeInfo { get; private set; }
17 | public ICollection Tags { get; private set; }
18 |
19 | public MyAnimeListEntry(decimal? score, CompletionStatus status, int numEpisodesWatched, UncertainDate myStartDate,
20 | UncertainDate myFinishDate, DateTime myLastUpdate, MalAnimeInfoFromUserLookup animeInfo, ICollection tags)
21 | {
22 | Score = score;
23 | Status = status;
24 | NumEpisodesWatched = numEpisodesWatched;
25 | MyStartDate = myStartDate;
26 | MyFinishDate = myFinishDate;
27 | MyLastUpdate = myLastUpdate;
28 | AnimeInfo = animeInfo;
29 | Tags = tags;
30 | }
31 |
32 | public bool Equals(MyAnimeListEntry other)
33 | {
34 | if (other == null) return false;
35 | return this.AnimeInfo.AnimeId == other.AnimeInfo.AnimeId;
36 | }
37 |
38 | public override bool Equals(object obj)
39 | {
40 | return Equals(obj as MyAnimeListEntry);
41 | }
42 |
43 | public override int GetHashCode()
44 | {
45 | return AnimeInfo.AnimeId.GetHashCode();
46 | }
47 |
48 | public override string ToString()
49 | {
50 | return AnimeInfo.Title;
51 | }
52 | }
53 | }
54 |
55 | /*
56 | Copyright 2011 Greg Najda
57 |
58 | Licensed under the Apache License, Version 2.0 (the "License");
59 | you may not use this file except in compliance with the License.
60 | You may obtain a copy of the License at
61 |
62 | http://www.apache.org/licenses/LICENSE-2.0
63 |
64 | Unless required by applicable law or agreed to in writing, software
65 | distributed under the License is distributed on an "AS IS" BASIS,
66 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
67 | See the License for the specific language governing permissions and
68 | limitations under the License.
69 | */
--------------------------------------------------------------------------------
/MalApi/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyDescription("API to myanimelist.net")]
6 | [assembly: AssemblyCompany("")]
7 | [assembly: AssemblyProduct("MAL API")]
8 | [assembly: AssemblyCopyright("Copyright © Greg Najda 2017")]
9 | [assembly: AssemblyTrademark("")]
10 | [assembly: AssemblyCulture("")]
11 |
12 | [assembly: InternalsVisibleTo("MalApi.UnitTests")]
13 |
14 | // Setting ComVisible to false makes the types in this assembly not visible
15 | // to COM components. If you need to access a type in this assembly from
16 | // COM, set the ComVisible attribute to true on that type.
17 | [assembly: ComVisible(false)]
18 |
19 | // The following GUID is for the ID of the typelib if this project is exposed to COM
20 | [assembly: Guid("edb1e556-e533-49eb-b0fb-8830fc9b2e27")]
21 |
--------------------------------------------------------------------------------
/MalApi/RateLimitingMyAnimeListApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Diagnostics;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace MalApi
10 | {
11 | ///
12 | /// Limits MAL requests by waiting a given time period betwen each request.
13 | /// The time is measured from after one request is complete to before the next request is made.
14 | /// You might use this to be nice to MAL and avoid making requests as fast as possible.
15 | /// This class is thread-safe if the underlying API is.
16 | ///
17 | public class RateLimitingMyAnimeListApi : IMyAnimeListApi
18 | {
19 | private IMyAnimeListApi m_underlyingApi;
20 | private bool m_ownApi;
21 | public TimeSpan TimeBetweenRequests { get; private set; }
22 |
23 | private Stopwatch m_stopwatchStartedAtLastRequest = null;
24 | private SemaphoreSlim m_lock;
25 |
26 | public RateLimitingMyAnimeListApi(IMyAnimeListApi underlyingApi, TimeSpan timeBetweenRequests, bool ownApi = false)
27 | {
28 | m_underlyingApi = underlyingApi;
29 | TimeBetweenRequests = timeBetweenRequests;
30 | m_ownApi = ownApi;
31 | m_lock = new SemaphoreSlim(1, 1);
32 | }
33 |
34 | private async Task DoActionWithRateLimitingAsync(Func> asyncAction, CancellationToken cancellationToken)
35 | {
36 | // Wait your turn to get the lock. Only one MAL request can be outstanding at a time.
37 | await m_lock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
38 |
39 | // Make sure to release the lock no matter what happens.
40 | try
41 | {
42 | // Sleep if needed
43 | if (m_stopwatchStartedAtLastRequest != null)
44 | {
45 | TimeSpan timeSinceLastRequest = m_stopwatchStartedAtLastRequest.Elapsed;
46 | if (timeSinceLastRequest < TimeBetweenRequests)
47 | {
48 | TimeSpan timeToWait = TimeBetweenRequests - timeSinceLastRequest;
49 | Logging.Log.InfoFormat("Waiting {0} before making request.", timeToWait);
50 | await Task.Delay(timeToWait).ConfigureAwait(continueOnCapturedContext: false);
51 | }
52 | }
53 |
54 | // Do the MAL request
55 | TResult result = await asyncAction().ConfigureAwait(continueOnCapturedContext: false);
56 |
57 | // Start the stopwatch so the next request knows how long it has to wait
58 | if (m_stopwatchStartedAtLastRequest == null)
59 | {
60 | m_stopwatchStartedAtLastRequest = new Stopwatch();
61 | }
62 | m_stopwatchStartedAtLastRequest.Restart();
63 |
64 | return result;
65 | }
66 | finally
67 | {
68 | m_lock.Release();
69 | }
70 | }
71 |
72 | public Task GetAnimeListForUserAsync(string user, CancellationToken cancellationToken)
73 | {
74 | return DoActionWithRateLimitingAsync(() => m_underlyingApi.GetAnimeListForUserAsync(user, cancellationToken), cancellationToken);
75 | }
76 |
77 | public Task GetAnimeListForUserAsync(string user)
78 | {
79 | return GetAnimeListForUserAsync(user, CancellationToken.None);
80 | }
81 |
82 | public MalUserLookupResults GetAnimeListForUser(string user)
83 | {
84 | return GetAnimeListForUserAsync(user).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
85 | }
86 |
87 | public Task GetRecentOnlineUsersAsync(CancellationToken cancellationToken)
88 | {
89 | return DoActionWithRateLimitingAsync(() => m_underlyingApi.GetRecentOnlineUsersAsync(cancellationToken), cancellationToken);
90 | }
91 |
92 | public Task GetRecentOnlineUsersAsync()
93 | {
94 | return GetRecentOnlineUsersAsync(CancellationToken.None);
95 | }
96 |
97 | public RecentUsersResults GetRecentOnlineUsers()
98 | {
99 | return GetRecentOnlineUsersAsync().ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
100 | }
101 |
102 | public Task GetAnimeDetailsAsync(int animeId, CancellationToken cancellationToken)
103 | {
104 | return DoActionWithRateLimitingAsync(() => m_underlyingApi.GetAnimeDetailsAsync(animeId, cancellationToken), cancellationToken);
105 | }
106 |
107 | public Task GetAnimeDetailsAsync(int animeId)
108 | {
109 | return GetAnimeDetailsAsync(animeId, CancellationToken.None);
110 | }
111 |
112 | public AnimeDetailsResults GetAnimeDetails(int animeId)
113 | {
114 | return GetAnimeDetailsAsync(animeId).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
115 | }
116 |
117 | public void Dispose()
118 | {
119 | if (m_ownApi && m_underlyingApi != null)
120 | {
121 | m_underlyingApi.Dispose();
122 | }
123 | }
124 | }
125 | }
126 |
127 | /*
128 | Copyright 2017 Greg Najda
129 |
130 | Licensed under the Apache License, Version 2.0 (the "License");
131 | you may not use this file except in compliance with the License.
132 | You may obtain a copy of the License at
133 |
134 | http://www.apache.org/licenses/LICENSE-2.0
135 |
136 | Unless required by applicable law or agreed to in writing, software
137 | distributed under the License is distributed on an "AS IS" BASIS,
138 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
139 | See the License for the specific language governing permissions and
140 | limitations under the License.
141 | */
142 |
--------------------------------------------------------------------------------
/MalApi/RecentUsersResults.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | public class RecentUsersResults
9 | {
10 | public IList RecentUsers { get; private set; }
11 |
12 | public RecentUsersResults(IList recentUsers)
13 | {
14 | RecentUsers = recentUsers;
15 | }
16 | }
17 | }
18 |
19 | /*
20 | Copyright 2012 Greg Najda
21 |
22 | Licensed under the Apache License, Version 2.0 (the "License");
23 | you may not use this file except in compliance with the License.
24 | You may obtain a copy of the License at
25 |
26 | http://www.apache.org/licenses/LICENSE-2.0
27 |
28 | Unless required by applicable law or agreed to in writing, software
29 | distributed under the License is distributed on an "AS IS" BASIS,
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 | See the License for the specific language governing permissions and
32 | limitations under the License.
33 | */
34 |
--------------------------------------------------------------------------------
/MalApi/RetryOnFailureMyAnimeListApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace MalApi
9 | {
10 | ///
11 | /// Retries failed requests after waiting for a short period. After a certain number of failures, it gives up and propagates the
12 | /// thrown exception.
13 | ///
14 | public class RetryOnFailureMyAnimeListApi : IMyAnimeListApi
15 | {
16 | private IMyAnimeListApi m_underlyingApi;
17 | private bool m_ownApi;
18 | private int m_numTriesBeforeGivingUp;
19 | private int m_timeBetweenRetriesInMs;
20 |
21 | public RetryOnFailureMyAnimeListApi(IMyAnimeListApi underlyingApi, int numTriesBeforeGivingUp, int timeBetweenRetriesInMs, bool ownApi = false)
22 | {
23 | m_underlyingApi = underlyingApi;
24 | m_numTriesBeforeGivingUp = numTriesBeforeGivingUp;
25 | m_timeBetweenRetriesInMs = timeBetweenRetriesInMs;
26 | m_ownApi = ownApi;
27 | }
28 |
29 | private async Task DoActionWithRetryAsync(Func> asyncAction, string baseErrorMessage)
30 | {
31 | int numTries = 0;
32 | while (true)
33 | {
34 | try
35 | {
36 | return await asyncAction().ConfigureAwait(continueOnCapturedContext: false);
37 | }
38 | catch (MalApiRequestException ex)
39 | {
40 | numTries++;
41 | Logging.Log.ErrorFormat("{0} (failure {1}): {2}", ex, baseErrorMessage, numTries, ex.Message);
42 |
43 | if (numTries < m_numTriesBeforeGivingUp)
44 | {
45 | Logging.Log.InfoFormat("Waiting {0} ms before trying again.", m_timeBetweenRetriesInMs);
46 | await Task.Delay(m_timeBetweenRetriesInMs).ConfigureAwait(continueOnCapturedContext: false);
47 | }
48 | else
49 | {
50 | throw;
51 | }
52 | }
53 | }
54 | }
55 |
56 | public Task GetAnimeListForUserAsync(string user, CancellationToken cancellationToken)
57 | {
58 | return DoActionWithRetryAsync(() => m_underlyingApi.GetAnimeListForUserAsync(user, cancellationToken),
59 | baseErrorMessage: string.Format("Error getting anime list for user {0}", user));
60 | }
61 |
62 | public Task GetAnimeListForUserAsync(string user)
63 | {
64 | return GetAnimeListForUserAsync(user, CancellationToken.None);
65 | }
66 |
67 | public MalUserLookupResults GetAnimeListForUser(string user)
68 | {
69 | return GetAnimeListForUserAsync(user).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
70 | }
71 |
72 | public Task GetRecentOnlineUsersAsync(CancellationToken cancellationToken)
73 | {
74 | return DoActionWithRetryAsync(() => m_underlyingApi.GetRecentOnlineUsersAsync(cancellationToken),
75 | baseErrorMessage: "Error getting recently active MAL users");
76 | }
77 |
78 | public Task GetRecentOnlineUsersAsync()
79 | {
80 | return GetRecentOnlineUsersAsync(CancellationToken.None);
81 | }
82 |
83 | public RecentUsersResults GetRecentOnlineUsers()
84 | {
85 | return GetRecentOnlineUsersAsync().ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
86 | }
87 |
88 | public Task GetAnimeDetailsAsync(int animeId, CancellationToken cancellationToken)
89 | {
90 | return DoActionWithRetryAsync(() => m_underlyingApi.GetAnimeDetailsAsync(animeId, cancellationToken),
91 | baseErrorMessage: string.Format("Error getting details for anime id {0}", animeId));
92 | }
93 |
94 | public Task GetAnimeDetailsAsync(int animeId)
95 | {
96 | return GetAnimeDetailsAsync(animeId, CancellationToken.None);
97 | }
98 |
99 | public AnimeDetailsResults GetAnimeDetails(int animeId)
100 | {
101 | return GetAnimeDetailsAsync(animeId).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
102 | }
103 |
104 | public void Dispose()
105 | {
106 | if (m_ownApi && m_underlyingApi != null)
107 | {
108 | m_underlyingApi.Dispose();
109 | }
110 | }
111 | }
112 | }
113 |
114 | /*
115 | Copyright 2017 Greg Najda
116 |
117 | Licensed under the Apache License, Version 2.0 (the "License");
118 | you may not use this file except in compliance with the License.
119 | You may obtain a copy of the License at
120 |
121 | http://www.apache.org/licenses/LICENSE-2.0
122 |
123 | Unless required by applicable law or agreed to in writing, software
124 | distributed under the License is distributed on an "AS IS" BASIS,
125 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
126 | See the License for the specific language governing permissions and
127 | limitations under the License.
128 | */
--------------------------------------------------------------------------------
/MalApi/UncertainDate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MalApi
7 | {
8 | ///
9 | /// Date where the year, month, and day can be unknown or not specified. A year of 2005, month of 7, and null day would indicate
10 | /// July 2005.
11 | ///
12 | public struct UncertainDate : IEquatable
13 | {
14 | public int? Year { get; private set; }
15 |
16 | private int? m_month;
17 | public int? Month
18 | {
19 | get { return m_month; }
20 | set
21 | {
22 | if (value < 1 || value > 12)
23 | {
24 | throw new ArgumentOutOfRangeException(string.Format("Month cannot be {0}.", value));
25 | }
26 | m_month = value;
27 | }
28 | }
29 |
30 | private int? m_day;
31 | public int? Day
32 | {
33 | get { return m_day; }
34 | set
35 | {
36 | if (value < 1 || value > 31)
37 | {
38 | throw new ArgumentOutOfRangeException(string.Format("Day cannot be {0}.", value));
39 | }
40 | m_day = value;
41 | }
42 | }
43 |
44 | public UncertainDate(int? year, int? month, int? day)
45 | : this()
46 | {
47 | Year = year;
48 | Month = month;
49 | Day = day;
50 | }
51 |
52 | public static UncertainDate Unknown { get { return new UncertainDate(); } }
53 |
54 | ///
55 | /// Creates an UncertainDate from the format MAL uses in its XML, YYYY-MM-DD with 00 or 0000 indicating an unknown.
56 | /// 2005-07-00 would indicate July 2005.
57 | ///
58 | ///
59 | ///
60 | public static UncertainDate FromMalDateString(string malDateString)
61 | {
62 | string[] yearMonthDay = malDateString.Split('-');
63 | if (yearMonthDay.Length != 3)
64 | {
65 | throw new FormatException(string.Format("{0} is not in YYYY-MM-DD format.", malDateString));
66 | }
67 |
68 | int? year = int.Parse(yearMonthDay[0]);
69 | if (year == 0) year = null;
70 |
71 | int? month = int.Parse(yearMonthDay[1]);
72 | if (month == 0) month = null;
73 |
74 | int? day = int.Parse(yearMonthDay[2]);
75 | if (day == 0) day = null;
76 |
77 | return new UncertainDate(year: year, month: month, day: day);
78 | }
79 |
80 | public bool Equals(UncertainDate other)
81 | {
82 | return this.Year == other.Year && this.Month == other.Month && this.Day == other.Day;
83 | }
84 |
85 | public override bool Equals(object obj)
86 | {
87 | if (obj is UncertainDate)
88 | return Equals((UncertainDate)obj);
89 | else
90 | return false;
91 | }
92 |
93 | public override int GetHashCode()
94 | {
95 | unchecked
96 | {
97 | int hash = 23;
98 | if (Year != null) hash = hash * 17 + Year.Value;
99 | if (Month != null) hash = hash * 17 + Month.Value;
100 | if (Day != null) hash = hash * 17 + Day.Value;
101 | return hash;
102 | }
103 | }
104 |
105 | public override string ToString()
106 | {
107 | string year;
108 | string month;
109 | string day;
110 |
111 | if (Year == null)
112 | year = "0000";
113 | else
114 | year = Year.Value.ToString("D4");
115 |
116 | if (Month == null)
117 | month = "00";
118 | else
119 | month = Month.Value.ToString("D2");
120 |
121 | if (Day == null)
122 | day = "00";
123 | else
124 | day = Day.Value.ToString("D2");
125 |
126 | return string.Format("{0}-{1}-{2}", year, month, day);
127 | }
128 |
129 | ///
130 | /// Creates a DateTime from UncertainDate, if possible, null otherwise
131 | ///
132 | ///
133 | public DateTime? ToDateTime()
134 | {
135 | if (Year != null && Month != null && Day != null)
136 | return new DateTime((int)Year, (int)Month, (int)Day);
137 |
138 | return null;
139 | }
140 | }
141 | }
142 |
143 | /*
144 | Copyright 2012 Greg Najda
145 |
146 | Licensed under the Apache License, Version 2.0 (the "License");
147 | you may not use this file except in compliance with the License.
148 | You may obtain a copy of the License at
149 |
150 | http://www.apache.org/licenses/LICENSE-2.0
151 |
152 | Unless required by applicable law or agreed to in writing, software
153 | distributed under the License is distributed on an "AS IS" BASIS,
154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
155 | See the License for the specific language governing permissions and
156 | limitations under the License.
157 | */
158 |
--------------------------------------------------------------------------------
/MalApi/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/MalApi/readme.txt:
--------------------------------------------------------------------------------
1 | This project targets both .NET 4.5 and netstandard 1.3 by using the new style of csproj ("sdk project"). The .csproj contains some manually specified properties.
2 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/notice.txt:
--------------------------------------------------------------------------------
1 | MalApi
2 | Copyright 2017 Greg Najda, licensed under the Apache License 2.0 (license.txt)
3 |
4 |
5 | MalApi uses the following libraries:
6 |
7 | Common.Logging is copyright by the Common.Logging developers and is licensed under the Apache License 2.0.
8 |
--------------------------------------------------------------------------------
/packages/repositories.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | myanimelist.net's API was taken down in 2018. There is now a new API, but I no longer watch anime and am not interested in investing the time to update this library.
2 |
3 | ----
4 |
5 | MalApi is a .NET library written in C# for accessing the myanimelist.net API or using scraping methods where no official API is available. Using it is easy:
6 |
7 | ```C#
8 | using (MyAnimeListApi api = new MyAnimeListApi())
9 | {
10 | api.UserAgent = "my_app"; // MAL now requires applications to be whitelisted. Whitelisted applications identify themselves by their user agent.
11 | MalUserLookupResults userLookup = api.GetAnimeListForUser("LordHighCaptain");
12 | foreach (MyAnimeListEntry listEntry in userLookup.AnimeList)
13 | {
14 | Console.WriteLine("Rating for {0}: {1}", listEntry.AnimeInfo.Title, listEntry.Score);
15 | }
16 | }
17 | ```
18 |
19 | Binaries are available as a [NuGet package](https://www.nuget.org/packages/MalApi/) called MalApi. The package supports .NET 4.5.2 and netstandard 1.3.
20 |
21 | MalApi currently contains these MAL functions:
22 | - Getting a user's anime list via http://myanimelist.net/malappinfo.php?status=all&type=anime&u=username. MalApi handles even malformed XML that MAL can generate when users put certain characters in tags.
23 | - Getting a list of recently online users from http://myanimelist.net/users.php
24 | - Getting an anime's genres from the anime's page.
25 |
26 | Also included are some useful implementations of IMyAnimeListApi that wrap another IMyAnimeListApi.
27 | - CachingMyAnimeListApi caches user lookups for a configurable amount of time.
28 | - RateLimitingMyAnimeListApi limits MAL requests to once every N milliseconds so you can throttle your requests if you are making a large number of them.
29 | - RetryOnFailureMyAnimeListApi waits a short period before retrying a request if a request fails. After a certain number of failures, it will give up.
30 |
31 | MalApi can be configured to log using any logging library compatible with Common.Logging. See App.config in the MalApi.Example project. MalApi will use the logger name "MAL API". Consult the Common.Logging and NLog documentation for more information about logging.
32 |
33 | MalApi is licensed under the Apache License 2.0.
--------------------------------------------------------------------------------