├── .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 < > & ' " &#x; &#; 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. --------------------------------------------------------------------------------