commit 9bb361922a5ef4ad290958282e5957995ca8e81d Author: Luke Pulverenti Date: Mon Oct 10 16:10:58 2016 -0400 initial diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8079ae2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27d4086 --- /dev/null +++ b/.gitignore @@ -0,0 +1,230 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +################# +## Media Browser +################# +ProgramData*/ +ProgramData-Server*/ +ProgramData-UI*/ + +################# +## Visual Studio +################# + +.vs + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc +*.scc +*.psess +*.vsp +*.vspx +*.orig +*.rej +*.sdf +*.opensdf +*.ipch + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 0000000..c41a0d0 Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 0000000..d3befda --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,153 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) + $([System.IO.Path]::Combine($(SolutionDir), "packages")) + + + + + $(SolutionDir).nuget + packages.config + $(SolutionDir)packages + + + + + $(NuGetToolsPath)\nuget.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 $(NuGetExePath) + + $(TargetDir.Trim('\\')) + + -RequireConsent + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -o "$(PackagesDir)" + $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(ResolveReferencesDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b34da37 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) Media Browser http://mediabrowser.tv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef9ce18 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +Emby.Plugins +==================== + +This repository contains many of the plugins that are built and maintained by the Emby community. + +Each of the projects has a build event that copies it's output to the programdata/plugins folder. + +By default this assumes you have the server repository side by side in a folder called 'MediaBrowser'. If this is not the case, or if you've installed the server than you'll need to update the build events manually in order to test code changes. + + +## More Information ## + +[How to Build a Server Plugin](https://github.com/MediaBrowser/MediaBrowser/wiki/How-to-build-a-Server-Plugin) diff --git a/SharedVersion.cs b/SharedVersion.cs new file mode 100644 index 0000000..261ee6a --- /dev/null +++ b/SharedVersion.cs @@ -0,0 +1,3 @@ +using System.Reflection; + +[assembly: AssemblyVersion("3.0.*")] diff --git a/Trakt.sln b/Trakt.sln new file mode 100644 index 0000000..4b6396e --- /dev/null +++ b/Trakt.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F17571BB-66D4-4DB5-8EC0-33A76DD5B018}" + ProjectSection(SolutionItems) = preProject + SharedVersion.cs = SharedVersion.cs + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trakt", "Trakt\Trakt.csproj", "{7FFC306B-2680-49C7-8BE0-6358B2A8A409}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7FFC306B-2680-49C7-8BE0-6358B2A8A409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FFC306B-2680-49C7-8BE0-6358B2A8A409}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FFC306B-2680-49C7-8BE0-6358B2A8A409}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FFC306B-2680-49C7-8BE0-6358B2A8A409}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FFC306B-2680-49C7-8BE0-6358B2A8A409}.Release|Any CPU.Build.0 = Release|Any CPU + {7FFC306B-2680-49C7-8BE0-6358B2A8A409}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs b/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs new file mode 100644 index 0000000..10dcf71 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktEpisode + { + [DataMember(Name = "season", EmitDefaultValue = false)] + public int? Season { get; set; } + + [DataMember(Name = "number", EmitDefaultValue = false)] + public int? Number { get; set; } + + [DataMember(Name = "title", EmitDefaultValue = false)] + public string Title { get; set; } + + [DataMember(Name = "ids")] + public TraktEpisodeId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs b/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs new file mode 100644 index 0000000..0e47e27 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktEpisodeId : TraktId + { + [DataMember(Name = "imdb", EmitDefaultValue = false)] + public string Imdb { get; set; } + + [DataMember(Name = "tmdb", EmitDefaultValue = false)] + public int? Tmdb { get; set; } + + [DataMember(Name = "tvdb", EmitDefaultValue = false)] + public int? Tvdb { get; set; } + + [DataMember(Name = "tvrage", EmitDefaultValue = false)] + public int? TvRage { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktId.cs b/Trakt/Api/DataContracts/BaseModel/TraktId.cs new file mode 100644 index 0000000..60473b1 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktId.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktId + { + [DataMember(Name = "trakt", EmitDefaultValue = false)] + public int? Trakt { get; set; } + + [DataMember(Name = "slug", EmitDefaultValue = false)] + public string Slug { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs b/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs new file mode 100644 index 0000000..6a5131c --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktMovie + { + [DataMember(Name = "title", EmitDefaultValue = false)] + public string Title { get; set; } + + [DataMember(Name = "year", EmitDefaultValue = false)] + public int? Year { get; set; } + + [DataMember(Name = "ids")] + public TraktMovieId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs b/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs new file mode 100644 index 0000000..1d2898a --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktMovieId : TraktId + { + [DataMember(Name = "imdb", EmitDefaultValue = false)] + public string Imdb { get; set; } + + [DataMember(Name = "tmdb", EmitDefaultValue = false)] + public int? Tmdb { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs b/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs new file mode 100644 index 0000000..e80a971 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktPerson + { + [DataMember(Name = "name", EmitDefaultValue = false)] + public string Name { get; set; } + + [DataMember(Name = "ids")] + public TraktPersonId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs b/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs new file mode 100644 index 0000000..816e351 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktPersonId : TraktId + { + [DataMember(Name = "imdb")] + public string ImdbId { get; set; } + + [DataMember(Name = "tmdb")] + public int? TmdbId { get; set; } + + [DataMember(Name = "tvrage")] + public int? TvRageId { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktRated.cs b/Trakt/Api/DataContracts/BaseModel/TraktRated.cs new file mode 100644 index 0000000..b23af3c --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktRated.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public abstract class TraktRated + { + [DataMember(Name = "rating")] + public int? Rating { get; set; } + + [DataMember(Name = "rated_at")] + public string RatedAt { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs b/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs new file mode 100644 index 0000000..3d5a424 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktSeason + { + [DataMember(Name = "number", EmitDefaultValue = false)] + public int? Number { get; set; } + + [DataMember(Name = "ids")] + public TraktSeasonId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs b/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs new file mode 100644 index 0000000..4d1f88f --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktSeasonId : TraktId + { + [DataMember(Name = "tmdb")] + public int? Tmdb { get; set; } + + [DataMember(Name = "tvdb")] + public int? Tvdb { get; set; } + + [DataMember(Name = "tvrage")] + public int? TvRage { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktShow.cs b/Trakt/Api/DataContracts/BaseModel/TraktShow.cs new file mode 100644 index 0000000..7efdc90 --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktShow.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktShow + { + [DataMember(Name = "title", EmitDefaultValue = false)] + public string Title { get; set; } + + [DataMember(Name = "year", EmitDefaultValue = false)] + public int? Year { get; set; } + + [DataMember(Name = "ids")] + public TraktShowId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs b/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs new file mode 100644 index 0000000..6862d6d --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktShowId : TraktId + { + [DataMember(Name = "imdb", EmitDefaultValue = false)] + public string Imdb { get; set; } + + [DataMember(Name = "tmdb", EmitDefaultValue = false)] + public int? Tmdb { get; set; } + + [DataMember(Name = "tvdb", EmitDefaultValue = false)] + public int? Tvdb { get; set; } + + [DataMember(Name = "tvrage", EmitDefaultValue = false)] + public int? TvRage { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/BaseModel/TraktUserSummary.cs b/Trakt/Api/DataContracts/BaseModel/TraktUserSummary.cs new file mode 100644 index 0000000..624af2b --- /dev/null +++ b/Trakt/Api/DataContracts/BaseModel/TraktUserSummary.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel +{ + [DataContract] + public class TraktUserSummary + { + [DataMember(Name = "username")] + public string Username { get; set; } + + [DataMember(Name = "name")] + public string FullName { get; set; } + + [DataMember(Name = "vip")] + public bool IsVip { get; set; } + + [DataMember(Name = "private")] + public bool IsPrivate { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Comments/TraktComment.cs b/Trakt/Api/DataContracts/Comments/TraktComment.cs new file mode 100644 index 0000000..1dc6b77 --- /dev/null +++ b/Trakt/Api/DataContracts/Comments/TraktComment.cs @@ -0,0 +1,39 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Comments +{ + [DataContract] + public class TraktComment + { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "parent_id")] + public int? ParentId { get; set; } + + [DataMember(Name = "created_at")] + public string CreatedAt { get; set; } + + [DataMember(Name = "comment")] + public string Comment { get; set; } + + [DataMember(Name = "spoiler")] + public bool IsSpoiler { get; set; } + + [DataMember(Name = "review")] + public bool IsReview { get; set; } + + [DataMember(Name = "replies")] + public int Replies { get; set; } + + [DataMember(Name = "likes")] + public int Likes { get; set; } + + [DataMember(Name = "user_rating")] + public int? UserRating { get; set; } + + [DataMember(Name = "user")] + public TraktUserSummary User { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs new file mode 100644 index 0000000..e47b628 --- /dev/null +++ b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Scrobble +{ + [DataContract] + public class TraktScrobbleEpisode + { + [DataMember(Name = "show", EmitDefaultValue = false)] + public TraktShow Show { get; set; } + + [DataMember(Name = "episode")] + public TraktEpisode Episode { get; set; } + + [DataMember(Name = "progress")] + public float Progress { get; set; } + + [DataMember(Name = "app_version")] + public string AppVersion { get; set; } + + [DataMember(Name = "app_date")] + public string AppDate { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs new file mode 100644 index 0000000..4c1db09 --- /dev/null +++ b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs @@ -0,0 +1,21 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Scrobble +{ + [DataContract] + public class TraktScrobbleMovie + { + [DataMember(Name = "movie")] + public TraktMovie Movie { get; set; } + + [DataMember(Name = "progress")] + public float Progress { get; set; } + + [DataMember(Name = "app_version")] + public string AppVersion { get; set; } + + [DataMember(Name = "app_date")] + public string AppDate { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs new file mode 100644 index 0000000..064cdf6 --- /dev/null +++ b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Scrobble +{ + [DataContract] + public class TraktScrobbleResponse + { + [DataMember(Name = "action")] + public string Action { get; set; } + + [DataMember(Name = "progress")] + public float Progress { get; set; } + + [DataMember(Name = "sharing")] + public SocialMedia Sharing { get; set; } + + [DataContract] + public class SocialMedia + { + [DataMember(Name = "facebook")] + public bool Facebook { get; set; } + + [DataMember(Name = "twitter")] + public bool Twitter { get; set; } + + [DataMember(Name = "tumblr")] + public bool Tumblr { get; set; } + } + + [DataMember(Name = "movie")] + public TraktMovie Movie { get; set; } + + [DataMember(Name = "episode")] + public TraktEpisode Episode { get; set; } + + [DataMember(Name = "show")] + public TraktShow Show { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs new file mode 100644 index 0000000..612b397 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs @@ -0,0 +1,27 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Collection +{ + [DataContract] + public class TraktEpisodeCollected : TraktEpisode + { + [DataMember(Name = "collected_at", EmitDefaultValue = false)] + public string CollectedAt { get; set; } + + [DataMember(Name = "media_type", EmitDefaultValue = false)] + public string MediaType { get; set; } + + [DataMember(Name = "resolution", EmitDefaultValue = false)] + public string Resolution { get; set; } + + [DataMember(Name = "audio", EmitDefaultValue = false)] + public string Audio { get; set; } + + [DataMember(Name = "audio_channels", EmitDefaultValue = false)] + public string AudioChannels { get; set; } + + [DataMember(Name = "3d", EmitDefaultValue = false)] + public bool Is3D { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs new file mode 100644 index 0000000..3f0163c --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs @@ -0,0 +1,27 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Collection +{ + [DataContract] + public class TraktMovieCollected : TraktMovie + { + [DataMember(Name = "collected_at", EmitDefaultValue = false)] + public string CollectedAt { get; set; } + + [DataMember(Name = "media_type", EmitDefaultValue = false)] + public string MediaType { get; set; } + + [DataMember(Name = "resolution", EmitDefaultValue = false)] + public string Resolution { get; set; } + + [DataMember(Name = "audio", EmitDefaultValue = false)] + public string Audio { get; set; } + + [DataMember(Name = "audio_channels", EmitDefaultValue = false)] + public string AudioChannels { get; set; } + + [DataMember(Name = "3d", EmitDefaultValue = false)] + public bool Is3D { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs new file mode 100644 index 0000000..5190d1c --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Collection +{ + [DataContract] + public class TraktShowCollected : TraktShow + { + [DataMember(Name = "seasons")] + public List Seasons { get; set; } + + [DataContract] + public class TraktSeasonCollected + { + [DataMember(Name = "number")] + public int Number { get; set; } + + [DataMember(Name = "episodes")] + public List Episodes { get; set; } + } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs new file mode 100644 index 0000000..4dec393 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Ratings +{ + [DataContract] + public class TraktEpisodeRated : TraktRated + { + [DataMember(Name = "number", EmitDefaultValue = false)] + public int? Number { get; set; } + + [DataMember(Name = "ids")] + public TraktEpisodeId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs new file mode 100644 index 0000000..50346a7 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Ratings +{ + [DataContract] + public class TraktMovieRated : TraktRated + { + [DataMember(Name = "title", EmitDefaultValue = false)] + public string Title { get; set; } + + [DataMember(Name = "year", EmitDefaultValue = false)] + public int? Year { get; set; } + + [DataMember(Name = "ids")] + public TraktMovieId Ids { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs new file mode 100644 index 0000000..96bc3b4 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Ratings +{ + [DataContract] + public class TraktShowRated : TraktRated + { + [DataMember(Name = "title", EmitDefaultValue = false)] + public string Title { get; set; } + + [DataMember(Name = "year", EmitDefaultValue = false)] + public int? Year { get; set; } + + [DataMember(Name = "ids")] + public TraktShowId Ids { get; set; } + + [DataMember(Name = "seasons")] + public List Seasons { get; set; } + + public class TraktSeasonRated : TraktRated + { + [DataMember(Name = "number")] + public int? Number { get; set; } + + [DataMember(Name = "episodes")] + public List Episodes { get; set; } + } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/TraktSync.cs b/Trakt/Api/DataContracts/Sync/TraktSync.cs new file mode 100644 index 0000000..2d80f18 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/TraktSync.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.Sync.Collection; +using Trakt.Api.DataContracts.Sync.Ratings; +using Trakt.Api.DataContracts.Sync.Watched; + +namespace Trakt.Api.DataContracts.Sync +{ + [DataContract] + public class TraktSync + { + [DataMember(Name = "movies", EmitDefaultValue = false)] + public List Movies { get; set; } + + [DataMember(Name = "shows", EmitDefaultValue = false)] + public List Shows { get; set; } + + [DataMember(Name = "episodes", EmitDefaultValue = false)] + public List Episodes { get; set; } + } + + [DataContract] + public class TraktSyncRated : TraktSync + { + } + + [DataContract] + public class TraktSyncWatched : TraktSync + { + } + + [DataContract] + public class TraktSyncCollected : TraktSync + { + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs b/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs new file mode 100644 index 0000000..6c66a24 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync +{ + [DataContract] + public class TraktSyncResponse + { + [DataMember(Name = "added")] + public Items Added { get; set; } + + [DataMember(Name = "deleted")] + public Items Deleted { get; set; } + + [DataMember(Name = "existing")] + public Items Existing { get; set; } + + [DataContract] + public class Items + { + [DataMember(Name = "movies")] + public int Movies { get; set; } + + [DataMember(Name = "shows")] + public int Shows { get; set; } + + [DataMember(Name = "seasons")] + public int Seasons { get; set; } + + [DataMember(Name = "episodes")] + public int Episodes { get; set; } + + [DataMember(Name = "people")] + public int People { get; set; } + } + + [DataMember(Name = "not_found")] + public NotFoundObjects NotFound { get; set; } + + [DataContract] + public class NotFoundObjects + { + [DataMember(Name = "movies")] + public List Movies { get; set; } + + [DataMember(Name = "shows")] + public List Shows { get; set; } + + [DataMember(Name = "episodes")] + public List Episodes { get; set; } + + [DataMember(Name = "seasons")] + public List Seasons { get; set; } + + [DataMember(Name = "people")] + public List People { get; set; } + } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs new file mode 100644 index 0000000..af4eacc --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Watched +{ + [DataContract] + public class TraktEpisodeWatched : TraktEpisode + { + [DataMember(Name = "watched_at", EmitDefaultValue = false)] + public string WatchedAt { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs new file mode 100644 index 0000000..a12d16c --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Watched +{ + [DataContract] + public class TraktMovieWatched : TraktMovie + { + [DataMember(Name = "watched_at", EmitDefaultValue = false)] + public string WatchedAt { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs new file mode 100644 index 0000000..c40cc82 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Watched +{ + [DataContract] + public class TraktSeasonWatched : TraktSeason + { + [DataMember(Name = "watched_at", EmitDefaultValue = false)] + public string WatchedAt { get; set; } + + [DataMember(Name = "episodes")] + public List Episodes { get; set; } + } +} diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs new file mode 100644 index 0000000..40e40ed --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Watched +{ + [DataContract] + public class TraktShowWatched : TraktShow + { + [DataMember(Name = "watched_at", EmitDefaultValue = false)] + public string WatchedAt { get; set; } + + [DataMember(Name = "seasons")] + public List Seasons { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/TraktUserToken.cs b/Trakt/Api/DataContracts/TraktUserToken.cs new file mode 100644 index 0000000..fd8872b --- /dev/null +++ b/Trakt/Api/DataContracts/TraktUserToken.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts +{ + [DataContract] + public class TraktUserToken + { + [DataMember(Name = "token")] + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/TraktUserTokenRequest.cs b/Trakt/Api/DataContracts/TraktUserTokenRequest.cs new file mode 100644 index 0000000..6af6b27 --- /dev/null +++ b/Trakt/Api/DataContracts/TraktUserTokenRequest.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts +{ + [DataContract] + public class TraktUserTokenRequest + { + [DataMember(Name = "login")] + public string Login { get; set; } + + [DataMember(Name = "password")] + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs b/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs new file mode 100644 index 0000000..00f1efd --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Trakt.Api.DataContracts.Users.Collection +{ + [DataContract] + public class TraktMetadata + { + [DataMember(Name = "media_type")] + public string MediaType { get; set; } + + [DataMember(Name = "resolution")] + public string Resolution { get; set; } + + [DataMember(Name = "audio")] + public string Audio { get; set; } + + [DataMember(Name = "audio_channels")] + public string AudioChannels { get; set; } + + [DataMember(Name = "3d")] + public bool Is3D { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs b/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs new file mode 100644 index 0000000..5e10bda --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Collection +{ + [DataContract] + public class TraktMovieCollected + { + [DataMember(Name = "collected_at")] + public string CollectedAt { get; set; } + + [DataMember(Name = "metadata")] + public TraktMetadata Metadata { get; set; } + + [DataMember(Name = "movie")] + public TraktMovie Movie { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs b/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs new file mode 100644 index 0000000..52acad4 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Collection +{ + [DataContract] + public class TraktShowCollected + { + [DataMember(Name = "last_collected_at")] + public string LastCollectedAt { get; set; } + + [DataMember(Name = "show")] + public TraktShow Show { get; set; } + + [DataMember(Name = "seasons")] + public List Seasons { get; set; } + + [DataContract] + public class TraktSeasonCollected + { + [DataMember(Name = "number")] + public int Number { get; set; } + + [DataMember(Name = "episodes")] + public List Episodes { get; set; } + + [DataContract] + public class TraktEpisodeCollected + { + [DataMember(Name = "number")] + public int Number { get; set; } + + [DataMember(Name = "collected_at")] + public string CollectedAt { get; set; } + + [DataMember(Name = "metadata")] + public TraktMetadata Metadata { get; set; } + } + } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs new file mode 100644 index 0000000..f9adee9 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Ratings +{ + [DataContract] + public class TraktEpisodeRated : TraktRated + { + [DataMember(Name = "episode")] + public TraktEpisode Episode { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs new file mode 100644 index 0000000..0ea2af9 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Ratings +{ + [DataContract] + public class TraktMovieRated : TraktRated + { + [DataMember(Name = "movie")] + public TraktMovie Movie { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs new file mode 100644 index 0000000..cef24a5 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Ratings +{ + [DataContract] + public class TraktSeasonRated : TraktRated + { + [DataMember(Name = "season")] + public TraktSeason Season { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs new file mode 100644 index 0000000..87e6a0e --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Ratings +{ + [DataContract] + public class TraktShowRated : TraktRated + { + [DataMember(Name = "show")] + public TraktShow Show { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs b/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs new file mode 100644 index 0000000..e5a4228 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Watched +{ + [DataContract] + public class TraktMovieWatched + { + [DataMember(Name = "plays")] + public int Plays { get; set; } + + [DataMember(Name = "last_watched_at")] + public string LastWatchedAt { get; set; } + + [DataMember(Name = "movie")] + public TraktMovie Movie { get; set; } + } +} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs b/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs new file mode 100644 index 0000000..a05d25d --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Users.Watched +{ + [DataContract] + public class TraktShowWatched + { + [DataMember(Name = "plays")] + public int Plays { get; set; } + + [DataMember(Name = "last_watched_at")] + public string WatchedAt { get; set; } + + [DataMember(Name = "show")] + public TraktShow Show { get; set; } + + [DataMember(Name = "seasons")] + public List Seasons { get; set; } + + [DataContract] + public class Season + { + [DataMember(Name = "number")] + public int Number { get; set; } + + [DataMember(Name = "episodes")] + public List Episodes { get; set; } + + [DataContract] + public class Episode + { + [DataMember(Name = "number")] + public int Number { get; set; } + + [DataMember(Name = "plays")] + public int Plays { get; set; } + } + } + } +} \ No newline at end of file diff --git a/Trakt/Api/ServerApiEndpoints.cs b/Trakt/Api/ServerApiEndpoints.cs new file mode 100644 index 0000000..d4e4487 --- /dev/null +++ b/Trakt/Api/ServerApiEndpoints.cs @@ -0,0 +1,197 @@ +using System; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using ServiceStack; +using Trakt.Helpers; + +namespace Trakt.Api +{ + /// + /// + /// + [Route("/Trakt/Users/{UserId}/Items/{Id}/Rate", "POST")] + [Api(Description = "Tell the Trakt server to send an item rating to trakt.tv")] + public class RateItem + { + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "Rating", Description = "Rating between 1 - 10 (0 = unrate)", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "POST")] + public int Rating { get; set; } + + } + + + + /// + /// + /// + [Route("/Trakt/Users/{UserId}/Items/{Id}/Comment", "POST")] + [Api(Description = "Tell the Trakt server to send an item comment to trakt.tv")] + public class CommentItem + { + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid Id { get; set; } + + [ApiMember(Name = "Comment", Description = "Text for the comment", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Comment { get; set; } + + [ApiMember(Name = "Spoiler", Description = "Set to true to indicate the comment contains spoilers. Defaults to false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool Spoiler { get; set; } + + [ApiMember(Name = "Review", Description = "Set to true to indicate the comment is a 200+ word review. Defaults to false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool Review { get; set; } + } + + + + /// + /// + /// + [Route("/Trakt/Users/{UserId}/RecommendedMovies", "POST")] + [Api(Description = "Request a list of recommended Movies based on a users watch history")] + public class RecommendedMovies + { + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + [ApiMember(Name = "Genre", Description = "Genre slug to filter by. (See http://trakt.tv/api-docs/genres-movies)", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public int Genre { get; set; } + + [ApiMember(Name = "StartYear", Description = "4-digit year to filter movies released this year or later", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int StartYear { get; set; } + + [ApiMember(Name = "EndYear", Description = "4-digit year to filter movies released this year or earlier", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int EndYear { get; set; } + + [ApiMember(Name = "HideCollected", Description = "Set true to hide movies in the users collection", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool HideCollected { get; set; } + + [ApiMember(Name = "HideWatchlisted", Description = "Set true to hide movies in the users watchlist", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool HideWatchlisted { get; set; } + } + + + + /// + /// + /// + [Route("/Trakt/Users/{UserId}/RecommendedShows", "POST")] + [Api(Description = "Request a list of recommended Shows based on a users watch history")] + public class RecommendedShows + { + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + [ApiMember(Name = "Genre", Description = "Genre slug to filter by. (See http://trakt.tv/api-docs/genres-shows)", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public int Genre { get; set; } + + [ApiMember(Name = "StartYear", Description = "4-digit year to filter shows released this year or later", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int StartYear { get; set; } + + [ApiMember(Name = "EndYear", Description = "4-digit year to filter shows released this year or earlier", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int EndYear { get; set; } + + [ApiMember(Name = "HideCollected", Description = "Set true to hide shows in the users collection", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool HideCollected { get; set; } + + [ApiMember(Name = "HideWatchlisted", Description = "Set true to hide shows in the users watchlist", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool HideWatchlisted { get; set; } + } + + + + /// + /// + /// + public class TraktUriService : IRestfulService + { + private readonly TraktApi _traktApi; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The trakt API. + /// The logger. + /// The library manager. + public TraktUriService(TraktApi traktApi, ILogger logger, ILibraryManager libraryManager) + { + _traktApi = traktApi; + _logger = logger; + _libraryManager = libraryManager; + } + + + + /// + /// + /// + /// + /// + public object Post(RateItem request) + { + _logger.Info("RateItem request received"); + + var currentItem = _libraryManager.GetItemById(new Guid(request.Id)); + + if (currentItem == null) + { + _logger.Info("currentItem is null"); + return null; + } + + return _traktApi.SendItemRating(currentItem, request.Rating, UserHelper.GetTraktUser(request.UserId)).Result; + + } + + + + /// + /// + /// + /// + /// + public object Post(CommentItem request) + { + _logger.Info("CommentItem request received"); + + var currentItem = _libraryManager.GetItemById(request.Id); + + return _traktApi.SendItemComment(currentItem, request.Comment, request.Spoiler, + UserHelper.GetTraktUser(request.UserId), request.Review).Result; + } + + + + /// + /// + /// + /// + /// + public object Post(RecommendedMovies request) + { + return _traktApi.SendMovieRecommendationsRequest(UserHelper.GetTraktUser(request.UserId)).Result; + } + + + + /// + /// + /// + /// + /// + public object Post(RecommendedShows request) + { + return _traktApi.SendShowRecommendationsRequest(UserHelper.GetTraktUser(request.UserId)).Result; + } + } +} diff --git a/Trakt/Api/TraktApi.cs b/Trakt/Api/TraktApi.cs new file mode 100644 index 0000000..3babe34 --- /dev/null +++ b/Trakt/Api/TraktApi.cs @@ -0,0 +1,1021 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using Trakt.Api.DataContracts; +using Trakt.Api.DataContracts.BaseModel; +using Trakt.Api.DataContracts.Scrobble; +using Trakt.Api.DataContracts.Sync; +using Trakt.Api.DataContracts.Sync.Ratings; +using Trakt.Api.DataContracts.Sync.Watched; +using Trakt.Helpers; +using Trakt.Model; +using MediaBrowser.Model.Entities; +using TraktMovieCollected = Trakt.Api.DataContracts.Sync.Collection.TraktMovieCollected; +using TraktEpisodeCollected = Trakt.Api.DataContracts.Sync.Collection.TraktEpisodeCollected; +using TraktShowCollected = Trakt.Api.DataContracts.Sync.Collection.TraktShowCollected; + +namespace Trakt.Api +{ + /// + /// + /// + public class TraktApi + { + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IServerApplicationHost _appHost; + private readonly IUserDataManager _userDataManager; + private readonly IFileSystem _fileSystem; + + public TraktApi(IJsonSerializer jsonSerializer, ILogger logger, IHttpClient httpClient, + IServerApplicationHost appHost, IUserDataManager userDataManager, IFileSystem fileSystem) + { + _httpClient = httpClient; + _appHost = appHost; + _userDataManager = userDataManager; + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + /// + /// Checks whether it's possible/allowed to sync a for a . + /// + /// + /// Item to check. + /// + /// + /// The trakt user to check for. + /// + /// + /// indicates if it's possible/allowed to sync this item. + /// + public bool CanSync(BaseItem item, TraktUser traktUser) + { + if (item.Path == null || item.LocationType == LocationType.Virtual) + { + return false; + } + + if (traktUser.LocationsExcluded != null && traktUser.LocationsExcluded.Any(s => _fileSystem.ContainsSubPath(s, item.Path))) + { + return false; + } + + var movie = item as Movie; + + if (movie != null) + { + return !string.IsNullOrEmpty(movie.GetProviderId(MetadataProviders.Imdb)) || + !string.IsNullOrEmpty(movie.GetProviderId(MetadataProviders.Tmdb)); + } + + var episode = item as Episode; + + if (episode != null && episode.Series != null && !episode.IsVirtualUnaired && !episode.IsMissingEpisode && (episode.IndexNumber.HasValue || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProviders.Tvdb)))) + { + var series = episode.Series; + + return !string.IsNullOrEmpty(series.GetProviderId(MetadataProviders.Imdb)) || + !string.IsNullOrEmpty(series.GetProviderId(MetadataProviders.Tvdb)); + } + + return false; + } + + /// + /// Report to trakt.tv that a movie is being watched, or has been watched. + /// + /// The movie being watched/scrobbled + /// MediaStatus enum dictating whether item is being watched or scrobbled + /// The user that watching the current movie + /// + /// A standard TraktResponse Data Contract + public async Task SendMovieStatusUpdateAsync(Movie movie, MediaStatus mediaStatus, TraktUser traktUser, float progressPercent) + { + var movieData = new TraktScrobbleMovie + { + AppDate = DateTime.Today.ToString("yyyy-MM-dd"), + AppVersion = _appHost.ApplicationVersion.ToString(), + Progress = progressPercent, + Movie = new TraktMovie + { + Title = movie.Name, + Year = movie.ProductionYear, + Ids = new TraktMovieId + { + Imdb = movie.GetProviderId(MetadataProviders.Imdb), + Tmdb = movie.GetProviderId(MetadataProviders.Tmdb).ConvertToInt() + } + } + }; + + string url; + switch (mediaStatus) + { + case MediaStatus.Watching: + url = TraktUris.ScrobbleStart; + break; + case MediaStatus.Paused: + url = TraktUris.ScrobblePause; + break; + default: + url = TraktUris.ScrobbleStop; + break; + } + + using (var response = await PostToTrakt(url, movieData, CancellationToken.None, traktUser)) + { + return _jsonSerializer.DeserializeFromStream(response); + } + } + + + /// + /// Reports to trakt.tv that an episode is being watched. Or that Episode(s) have been watched. + /// + /// The episode being watched + /// Enum indicating whether an episode is being watched or scrobbled + /// The user that's watching the episode + /// + /// A List of standard TraktResponse Data Contracts + public async Task> SendEpisodeStatusUpdateAsync(Episode episode, MediaStatus status, TraktUser traktUser, float progressPercent) + { + var episodeDatas = new List(); + var tvDbId = episode.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(tvDbId) && (!episode.IndexNumber.HasValue || !episode.IndexNumberEnd.HasValue || episode.IndexNumberEnd <= episode.IndexNumber)) + { + episodeDatas.Add(new TraktScrobbleEpisode + { + AppDate = DateTime.Today.ToString("yyyy-MM-dd"), + AppVersion = _appHost.ApplicationVersion.ToString(), + Progress = progressPercent, + Episode = new TraktEpisode + { + Ids = new TraktEpisodeId + { + Tvdb = tvDbId.ConvertToInt() + }, + } + }); + } + else if (episode.IndexNumber.HasValue) + { + var indexNumber = episode.IndexNumber.Value; + var finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; + + for (var number = indexNumber; number <= finalNumber; number++) + { + episodeDatas.Add(new TraktScrobbleEpisode + { + AppDate = DateTime.Today.ToString("yyyy-MM-dd"), + AppVersion = _appHost.ApplicationVersion.ToString(), + Progress = progressPercent, + Episode = new TraktEpisode + { + Season = episode.GetSeasonNumber(), + Number = number + }, + Show = new TraktShow + { + Title = episode.Series.Name, + Year = episode.Series.ProductionYear, + Ids = new TraktShowId + { + Tvdb = episode.Series.GetProviderId(MetadataProviders.Tvdb).ConvertToInt(), + Imdb = episode.Series.GetProviderId(MetadataProviders.Imdb), + TvRage = episode.Series.GetProviderId(MetadataProviders.TvRage).ConvertToInt() + } + } + }); + } + } + + string url; + switch (status) + { + case MediaStatus.Watching: + url = TraktUris.ScrobbleStart; + break; + case MediaStatus.Paused: + url = TraktUris.ScrobblePause; + break; + default: + url = TraktUris.ScrobbleStop; + break; + } + var responses = new List(); + foreach (var traktScrobbleEpisode in episodeDatas) + { + using (var response = await PostToTrakt(url, traktScrobbleEpisode, CancellationToken.None, traktUser)) + { + responses.Add(_jsonSerializer.DeserializeFromStream(response)); + } + } + return responses; + } + + /// + /// Add or remove a list of movies to/from the users trakt.tv library + /// + /// The movies to add + /// The user who's library is being updated + /// The cancellation token. + /// + /// Task{TraktResponseDataContract}. + public async Task> SendLibraryUpdateAsync(List movies, TraktUser traktUser, + CancellationToken cancellationToken, EventType eventType) + { + if (movies == null) + throw new ArgumentNullException("movies"); + if (traktUser == null) + throw new ArgumentNullException("traktUser"); + + if (eventType == EventType.Update) return null; + + var moviesPayload = movies.Select(m => + { + var audioStream = m.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); + var traktMovieCollected = new TraktMovieCollected + { + CollectedAt = m.DateCreated.ToISO8601(), + Title = m.Name, + Year = m.ProductionYear, + Ids = new TraktMovieId + { + Imdb = m.GetProviderId(MetadataProviders.Imdb), + Tmdb = m.GetProviderId(MetadataProviders.Tmdb).ConvertToInt() + } + }; + if (traktUser.ExportMediaInfo) + { + traktMovieCollected.Is3D = m.Is3D; + traktMovieCollected.AudioChannels = audioStream.GetAudioChannels(); + traktMovieCollected.Audio = audioStream.GetCodecRepresetation(); + traktMovieCollected.Resolution = m.GetDefaultVideoStream().GetResolution(); + } + return traktMovieCollected; + }).ToList(); + var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + + var responses = new List(); + var chunks = moviesPayload.ToChunks(100); + foreach (var chunk in chunks) + { + var data = new TraktSyncCollected + { + Movies = chunk.ToList() + }; + using (var response = await PostToTrakt(url, data, cancellationToken, traktUser)) + { + responses.Add(_jsonSerializer.DeserializeFromStream(response)); + } + } + return responses; + } + + + + /// + /// Add or remove a list of Episodes to/from the users trakt.tv library + /// + /// The episodes to add + /// The user who's library is being updated + /// The cancellation token. + /// + /// Task{TraktResponseDataContract}. + public async Task> SendLibraryUpdateAsync(IReadOnlyList episodes, + TraktUser traktUser, CancellationToken cancellationToken, EventType eventType) + { + if (episodes == null) + throw new ArgumentNullException("episodes"); + + if (traktUser == null) + throw new ArgumentNullException("traktUser"); + + if (eventType == EventType.Update) return null; + var responses = new List(); + var chunks = episodes.ToChunks(100); + foreach (var chunk in chunks) + { + responses.Add(await SendLibraryUpdateInternalAsync(chunk.ToList(), traktUser, cancellationToken, eventType)); + } + return responses; + } + + private async Task SendLibraryUpdateInternalAsync(IEnumerable episodes, + TraktUser traktUser, CancellationToken cancellationToken, EventType eventType) + { + var episodesPayload = new List(); + var showPayload = new List(); + foreach (Episode episode in episodes) + { + var audioStream = episode.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); + var tvDbId = episode.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(tvDbId) && + (!episode.IndexNumber.HasValue || !episode.IndexNumberEnd.HasValue || + episode.IndexNumberEnd <= episode.IndexNumber)) + { + var traktEpisodeCollected = new TraktEpisodeCollected + { + CollectedAt = episode.DateCreated.ToISO8601(), + Ids = new TraktEpisodeId + { + Tvdb = tvDbId.ConvertToInt() + } + }; + if (traktUser.ExportMediaInfo) + { + traktEpisodeCollected.Is3D = episode.Is3D; + traktEpisodeCollected.AudioChannels = audioStream.GetAudioChannels(); + traktEpisodeCollected.Audio = audioStream.GetCodecRepresetation(); + traktEpisodeCollected.Resolution = episode.GetDefaultVideoStream().GetResolution(); + } + episodesPayload.Add(traktEpisodeCollected); + } + else if (episode.IndexNumber.HasValue) + { + var indexNumber = episode.IndexNumber.Value; + var finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; + var syncShow = + showPayload.FirstOrDefault( + sre => + sre.Ids != null && + sre.Ids.Tvdb == episode.Series.GetProviderId(MetadataProviders.Tvdb).ConvertToInt()); + if (syncShow == null) + { + syncShow = new TraktShowCollected + { + Ids = new TraktShowId + { + Tvdb = episode.Series.GetProviderId(MetadataProviders.Tvdb).ConvertToInt(), + Imdb = episode.Series.GetProviderId(MetadataProviders.Imdb), + TvRage = episode.Series.GetProviderId(MetadataProviders.TvRage).ConvertToInt() + }, + Seasons = new List() + }; + showPayload.Add(syncShow); + } + var syncSeason = + syncShow.Seasons.FirstOrDefault(ss => ss.Number == episode.GetSeasonNumber()); + if (syncSeason == null) + { + syncSeason = new TraktShowCollected.TraktSeasonCollected + { + Number = episode.GetSeasonNumber(), + Episodes = new List() + }; + syncShow.Seasons.Add(syncSeason); + } + for (var number = indexNumber; number <= finalNumber; number++) + { + var traktEpisodeCollected = new TraktEpisodeCollected + { + Number = number, + CollectedAt = episode.DateCreated.ToISO8601(), + Ids = new TraktEpisodeId + { + Tvdb = tvDbId.ConvertToInt() + } + }; + if (traktUser.ExportMediaInfo) + { + traktEpisodeCollected.Is3D = episode.Is3D; + traktEpisodeCollected.AudioChannels = audioStream.GetAudioChannels(); + traktEpisodeCollected.Audio = audioStream.GetCodecRepresetation(); + traktEpisodeCollected.Resolution = episode.GetDefaultVideoStream().GetResolution(); + } + syncSeason.Episodes.Add(traktEpisodeCollected); + } + } + } + + var data = new TraktSyncCollected + { + Episodes = episodesPayload.ToList(), + Shows = showPayload.ToList() + }; + + var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + using (var response = await PostToTrakt(url, data, cancellationToken, traktUser)) + { + return _jsonSerializer.DeserializeFromStream(response); + } + } + + + + /// + /// Add or remove a Show(Series) to/from the users trakt.tv library + /// + /// The show to remove + /// The user who's library is being updated + /// The cancellation token. + /// + /// Task{TraktResponseDataContract}. + public async Task SendLibraryUpdateAsync(Series show, TraktUser traktUser, CancellationToken cancellationToken, EventType eventType) + { + if (show == null) + throw new ArgumentNullException("show"); + if (traktUser == null) + throw new ArgumentNullException("traktUser"); + + if (eventType == EventType.Update) return null; + + var showPayload = new List + { + new TraktShowCollected + { + Title = show.Name, + Year = show.ProductionYear, + Ids = new TraktShowId + { + Tvdb = show.GetProviderId(MetadataProviders.Tvdb).ConvertToInt(), + Imdb = show.GetProviderId(MetadataProviders.Imdb), + TvRage = show.GetProviderId(MetadataProviders.TvRage).ConvertToInt() + }, + } + }; + + var data = new TraktSyncCollected + { + Shows = showPayload.ToList() + }; + + var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + using (var response = await PostToTrakt(url, data, cancellationToken, traktUser)) + { + return _jsonSerializer.DeserializeFromStream(response); + } + } + + + + /// + /// Rate an item + /// + /// + /// + /// + /// + public async Task SendItemRating(BaseItem item, int rating, TraktUser traktUser) + { + object data = new {}; + if (item is Movie) + { + data = new + { + movies = new[] + { + new TraktMovieRated + { + Title = item.Name, + Year = item.ProductionYear, + Ids = new TraktMovieId + { + Imdb = item.GetProviderId(MetadataProviders.Imdb), + Tmdb = item.GetProviderId(MetadataProviders.Tmdb).ConvertToInt() + }, + Rating = rating + } + } + }; + + } + else if (item is Episode ) + { + var episode = item as Episode; + + if (string.IsNullOrEmpty(episode.GetProviderId(MetadataProviders.Tvdb))) + { + if (episode.IndexNumber.HasValue) + { + var indexNumber = episode.IndexNumber.Value; + var show = new TraktShowRated + { + Ids = new TraktShowId + { + Tvdb = episode.Series.GetProviderId(MetadataProviders.Tvdb).ConvertToInt(), + Imdb = episode.Series.GetProviderId(MetadataProviders.Imdb), + TvRage = episode.Series.GetProviderId(MetadataProviders.TvRage).ConvertToInt() + }, + Seasons = new List + { + new TraktShowRated.TraktSeasonRated + { + Number = episode.GetSeasonNumber(), + Episodes = new List + { + new TraktEpisodeRated + { + Number = indexNumber, + Rating = rating + } + } + } + } + }; + data = new + { + shows = new[] + { + show + } + }; + } + } + else + { + data = new + { + episodes = new[] + { + new TraktEpisodeRated + { + Rating = rating, + Ids = new TraktEpisodeId + { + Tvdb = episode.GetProviderId(MetadataProviders.Tvdb).ConvertToInt() + } + } + } + }; + } + } + else // It's a Series + { + data = new + { + shows = new[] + { + new TraktShowRated + { + Rating = rating, + Title = item.Name, + Year = item.ProductionYear, + Ids = new TraktShowId + { + Imdb = item.GetProviderId(MetadataProviders.Imdb), + Tvdb = item.GetProviderId(MetadataProviders.Tvdb).ConvertToInt() + } + } + } + }; + } + + using (var response = await PostToTrakt(TraktUris.SyncRatingsAdd, data, traktUser)) + { + return _jsonSerializer.DeserializeFromStream(response); + } + } + + + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task SendItemComment(BaseItem item, string comment, bool containsSpoilers, TraktUser traktUser, bool isReview = false) + { + return null; + //TODO: This functionallity is not available yet +// string url; +// var data = new Dictionary +// { +// {"username", traktUser.UserName}, +// {"password", traktUser.Password} +// }; +// +// if (item is Movie) +// { +// if (item.ProviderIds != null && item.ProviderIds.ContainsKey("Imdb")) +// data.Add("imdb_id", item.ProviderIds["Imdb"]); +// +// data.Add("title", item.Name); +// data.Add("year", item.ProductionYear != null ? item.ProductionYear.ToString() : ""); +// url = TraktUris.CommentMovie; +// } +// else +// { +// var episode = item as Episode; +// if (episode != null) +// { +// if (episode.Series.ProviderIds != null) +// { +// if (episode.Series.ProviderIds.ContainsKey("Imdb")) +// data.Add("imdb_id", episode.Series.ProviderIds["Imdb"]); +// +// if (episode.Series.ProviderIds.ContainsKey("Tvdb")) +// data.Add("tvdb_id", episode.Series.ProviderIds["Tvdb"]); +// } +// +// data.Add("season", episode.AiredSeasonNumber.ToString()); +// data.Add("episode", episode.IndexNumber.ToString()); +// url = TraktUris.CommentEpisode; +// } +// else // It's a Series +// { +// data.Add("title", item.Name); +// data.Add("year", item.ProductionYear != null ? item.ProductionYear.ToString() : ""); +// +// if (item.ProviderIds != null) +// { +// if (item.ProviderIds.ContainsKey("Imdb")) +// data.Add("imdb_id", item.ProviderIds["Imdb"]); +// +// if (item.ProviderIds.ContainsKey("Tvdb")) +// data.Add("tvdb_id", item.ProviderIds["Tvdb"]); +// } +// +// url = TraktUris.CommentShow; +// } +// } +// +// data.Add("comment", comment); +// data.Add("spoiler", containsSpoilers.ToString()); +// data.Add("review", isReview.ToString()); +// +// Stream response = +// await +// _httpClient.Post(url, data, Plugin.Instance.TraktResourcePool, +// CancellationToken.None).ConfigureAwait(false); +// +// return _jsonSerializer.DeserializeFromStream(response); + } + + /// + /// + /// + /// + /// + public async Task> SendMovieRecommendationsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.RecommendationsMovies, traktUser)) + { + return _jsonSerializer.DeserializeFromStream>(response); + } + } + + + + /// + /// + /// + /// + /// + public async Task> SendShowRecommendationsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.RecommendationsShows, traktUser)) + { + return _jsonSerializer.DeserializeFromStream>(response); + } + } + + + + /// + /// + /// + /// + /// + public async Task> SendGetAllWatchedMoviesRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.WatchedMovies, traktUser)) + { + return _jsonSerializer.DeserializeFromStream>(response); + } + } + + /// + /// + /// + /// + /// + public async Task> SendGetWatchedShowsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.WatchedShows, traktUser)) + { + return _jsonSerializer.DeserializeFromStream>(response); + } + } + + /// + /// + /// + /// + /// + public async Task> SendGetAllCollectedMoviesRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.CollectedMovies, traktUser)) + { + return _jsonSerializer.DeserializeFromStream>(response); + } + } + + /// + /// + /// + /// + /// + public async Task> SendGetCollectedShowsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.CollectedShows, traktUser)) + { + return _jsonSerializer.DeserializeFromStream>(response); + } + } + + /// + /// Send a list of movies to trakt.tv that have been marked watched or unwatched + /// + /// The list of movies to send + /// The trakt user profile that is being updated + /// True if movies are being marked seen, false otherwise + /// The Cancellation Token + /// + public async Task> SendMoviePlaystateUpdates(List movies, TraktUser traktUser, + bool seen, CancellationToken cancellationToken) + { + if (movies == null) + throw new ArgumentNullException("movies"); + if (traktUser == null) + throw new ArgumentNullException("traktUser"); + if (!traktUser.PostWatchedHistory) + return new List(); + + var moviesPayload = movies.Select(m => + { + var lastPlayedDate = seen + ? _userDataManager.GetUserData(new Guid(traktUser.LinkedMbUserId), m).LastPlayedDate + : null; + return new TraktMovieWatched + { + Title = m.Name, + Ids = new TraktMovieId + { + Imdb = m.GetProviderId(MetadataProviders.Imdb), + Tmdb = + string.IsNullOrEmpty(m.GetProviderId(MetadataProviders.Tmdb)) + ? (int?) null + : int.Parse(m.GetProviderId(MetadataProviders.Tmdb)) + }, + Year = m.ProductionYear, + WatchedAt = lastPlayedDate.HasValue ? lastPlayedDate.Value.ToISO8601() : null + }; + }).ToList(); + var chunks = moviesPayload.ToChunks(100).ToList(); + var traktResponses = new List(); + + foreach (var chunk in chunks) + { + var data = new TraktSyncWatched + { + Movies = chunk.ToList() + }; + var url = seen ? TraktUris.SyncWatchedHistoryAdd : TraktUris.SyncWatchedHistoryRemove; + + using (var response = await PostToTrakt(url, data, cancellationToken, traktUser)) + { + if (response != null) + traktResponses.Add(_jsonSerializer.DeserializeFromStream(response)); + } + } + return traktResponses; + } + + + + /// + /// Send a list of episodes to trakt.tv that have been marked watched or unwatched + /// + /// The list of episodes to send + /// The trakt user profile that is being updated + /// True if episodes are being marked seen, false otherwise + /// The Cancellation Token + /// + public async Task> SendEpisodePlaystateUpdates(List episodes, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + { + if (episodes == null) + throw new ArgumentNullException("episodes"); + + if (traktUser == null) + throw new ArgumentNullException("traktUser"); + if (!traktUser.PostWatchedHistory) + return new List(); + + var chunks = episodes.ToChunks(100).ToList(); + var traktResponses = new List(); + + foreach (var chunk in chunks) + { + var response = await SendEpisodePlaystateUpdatesInternalAsync(chunk, traktUser, seen, cancellationToken); + + if (response != null) + traktResponses.Add(response); + } + return traktResponses; + } + + + private async Task SendEpisodePlaystateUpdatesInternalAsync(IEnumerable episodeChunk, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + { + var data = new TraktSyncWatched{ Episodes = new List(), Shows = new List() }; + foreach (var episode in episodeChunk) + { + var tvDbId = episode.GetProviderId(MetadataProviders.Tvdb); + var lastPlayedDate = seen + ? _userDataManager.GetUserData(new Guid(traktUser.LinkedMbUserId), episode) + .LastPlayedDate + : null; + if (!string.IsNullOrEmpty(tvDbId) && (!episode.IndexNumber.HasValue || !episode.IndexNumberEnd.HasValue || episode.IndexNumberEnd <= episode.IndexNumber)) + { + + data.Episodes.Add(new TraktEpisodeWatched + { + Ids = new TraktEpisodeId + { + Tvdb = int.Parse(tvDbId) + }, + WatchedAt = lastPlayedDate.HasValue ? lastPlayedDate.Value.ToISO8601() : null + }); + } + else if (episode.IndexNumber != null) + { + var indexNumber = episode.IndexNumber.Value; + var finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; + + var syncShow = data.Shows.FirstOrDefault(sre => sre.Ids != null && sre.Ids.Tvdb == episode.Series.GetProviderId(MetadataProviders.Tvdb).ConvertToInt()); + if (syncShow == null) + { + syncShow = new TraktShowWatched + { + Ids = new TraktShowId + { + Tvdb = episode.Series.GetProviderId(MetadataProviders.Tvdb).ConvertToInt(), + Imdb = episode.Series.GetProviderId(MetadataProviders.Imdb), + TvRage = episode.Series.GetProviderId(MetadataProviders.TvRage).ConvertToInt() + }, + Seasons = new List() + }; + data.Shows.Add(syncShow); + } + var syncSeason = syncShow.Seasons.FirstOrDefault(ss => ss.Number == episode.GetSeasonNumber()); + if(syncSeason == null) + { + syncSeason = new TraktSeasonWatched + { + Number = episode.GetSeasonNumber(), + Episodes = new List() + }; + syncShow.Seasons.Add(syncSeason); + } + for (var number = indexNumber; number <= finalNumber; number++) + { + syncSeason.Episodes.Add(new TraktEpisodeWatched + { + Number = number, + WatchedAt = lastPlayedDate.HasValue ? lastPlayedDate.Value.ToISO8601() : null + }); + } + } + } + var url = seen ? TraktUris.SyncWatchedHistoryAdd : TraktUris.SyncWatchedHistoryRemove; + + using (var response = await PostToTrakt(url, data, cancellationToken, traktUser)) + { + return _jsonSerializer.DeserializeFromStream(response); + } + } + + public async Task GetUserToken(TraktUser traktUser) + { + var data = new TraktUserTokenRequest + { + Login = traktUser.UserName, + Password = traktUser.Password + }; + + using (var response = await PostToTrakt(TraktUris.Login, data, null)) + { + return _jsonSerializer.DeserializeFromStream(response); + } + } + + private Task GetFromTrakt(string url, TraktUser traktUser) + { + return GetFromTrakt(url, CancellationToken.None, traktUser); + } + + private async Task GetFromTrakt(string url, CancellationToken cancellationToken, TraktUser traktUser) + { + var options = new HttpRequestOptions + { + Url = url, + ResourcePool = Plugin.Instance.TraktResourcePool, + CancellationToken = cancellationToken, + RequestContentType = "application/json", + TimeoutMs = 120000, + LogErrorResponseBody = false, + LogRequest = true + }; + await SetRequestHeaders(options, traktUser); + + try + { + return await _httpClient.Get(options).ConfigureAwait(false); + } + catch + { + + } + + // Retry + return await _httpClient.Get(options).ConfigureAwait(false); + } + + private Task PostToTrakt(string url, object data, TraktUser traktUser) + { + return PostToTrakt(url, data, CancellationToken.None, traktUser); + } + + private async Task PostToTrakt(string url, object data, CancellationToken cancellationToken, TraktUser traktUser) + { + var requestContent = data == null? string.Empty : _jsonSerializer.SerializeToString(data); + if (traktUser != null && traktUser.ExtraLogging && url != TraktUris.Login) + { + _logger.Debug(requestContent); + } + var options = new HttpRequestOptions + { + Url = url, + ResourcePool = Plugin.Instance.TraktResourcePool, + CancellationToken = cancellationToken, + RequestContentType = "application/json", + RequestContent = requestContent, + TimeoutMs = 120000, + LogErrorResponseBody = false, + LogRequest = true + }; + await SetRequestHeaders(options, traktUser); + + try + { + var response = await _httpClient.Post(options).ConfigureAwait(false); + return response.Content; + } + catch + { + + } + + // retry + var retryResponse = await _httpClient.Post(options).ConfigureAwait(false); + return retryResponse.Content; + } + + private async Task SetRequestHeaders(HttpRequestOptions options, TraktUser traktUser) + { + options.RequestHeaders.Add("trakt-api-version", "2"); + options.RequestHeaders.Add("trakt-api-key", TraktUris.Devkey); + if (traktUser != null) + { + if (string.IsNullOrEmpty(traktUser.UserToken)) + { + var userToken = await GetUserToken(traktUser); + + if (userToken != null) + { + traktUser.UserToken = userToken.Token; + } + } + if (!string.IsNullOrEmpty(traktUser.UserToken)) + { + options.RequestHeaders.Add("trakt-user-login", traktUser.UserName); + options.RequestHeaders.Add("trakt-user-token", traktUser.UserToken); + } + } + } + } +} diff --git a/Trakt/Api/TraktURIs.cs b/Trakt/Api/TraktURIs.cs new file mode 100644 index 0000000..f002f75 --- /dev/null +++ b/Trakt/Api/TraktURIs.cs @@ -0,0 +1,43 @@ +namespace Trakt.Api +{ + public static class TraktUris + { + public const string Devkey = "0fabacd4bcf52604be7463374d2b9ee91995896ee410bb5ef9ce07ecc18db85c"; + + #region POST URI's + public const string Login = @"https://api-v2launch.trakt.tv/auth/login"; + + public const string SyncCollectionAdd = @"https://api-v2launch.trakt.tv/sync/collection"; + public const string SyncCollectionRemove = @"https://api-v2launch.trakt.tv/sync/collection/remove"; + public const string SyncWatchedHistoryAdd = @"https://api-v2launch.trakt.tv/sync/history"; + public const string SyncWatchedHistoryRemove = @"https://api-v2launch.trakt.tv/sync/history/remove"; + public const string SyncRatingsAdd = @"https://api-v2launch.trakt.tv/sync/ratings"; + + public const string ScrobbleStart = @"https://api-v2launch.trakt.tv/scrobble/start"; + public const string ScrobblePause = @"https://api-v2launch.trakt.tv/scrobble/pause"; + public const string ScrobbleStop = @"https://api-v2launch.trakt.tv/scrobble/stop"; + #endregion + + #region GET URI's + + public const string WatchedMovies = @"https://api-v2launch.trakt.tv/sync/watched/movies"; + public const string WatchedShows = @"https://api-v2launch.trakt.tv/sync/watched/shows"; + public const string CollectedMovies = @"https://api-v2launch.trakt.tv/sync/collection/movies?extended=metadata"; + public const string CollectedShows = @"https://api-v2launch.trakt.tv/sync/collection/shows?extended=metadata"; + + // Recommendations + public const string RecommendationsMovies = @"https://api-v2launch.trakt.tv/recommendations/movies"; + public const string RecommendationsShows = @"https://api-v2launch.trakt.tv/recommendations/shows"; + + #endregion + + #region DELETE + + // Recommendations + public const string RecommendationsMoviesDismiss = @"https://api-v2launch.trakt.tv/recommendations/movies/{0}"; + public const string RecommendationsShowsDismiss = @"https://api-v2launch.trakt.tv/recommendations/shows/{0}"; + + #endregion + } +} + diff --git a/Trakt/Configuration/PluginConfiguration.cs b/Trakt/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000..c3fa586 --- /dev/null +++ b/Trakt/Configuration/PluginConfiguration.cs @@ -0,0 +1,15 @@ +using MediaBrowser.Model.Plugins; +using Trakt.Model; + +namespace Trakt.Configuration +{ + public class PluginConfiguration : BasePluginConfiguration + { + public PluginConfiguration() + { + TraktUsers = new TraktUser[] {}; + } + + public TraktUser[] TraktUsers { get; set; } + } +} diff --git a/Trakt/Configuration/TraktConfigurationPage.cs b/Trakt/Configuration/TraktConfigurationPage.cs new file mode 100644 index 0000000..a94a407 --- /dev/null +++ b/Trakt/Configuration/TraktConfigurationPage.cs @@ -0,0 +1,35 @@ +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; +using System.IO; + +namespace Trakt.Configuration +{ + /// + /// Class TraktConfigurationPage + /// + class TraktConfigurationPage : IPluginConfigurationPage + { + /// + /// Gets the name. + /// + /// The name. + public string Name => "Trakt for MediaBrowser"; + + /// + /// Gets the HTML stream. + /// + /// Stream. + public Stream GetHtmlStream() + { + return GetType().Assembly.GetManifestResourceStream(GetType().Namespace + ".configPage.html"); + } + + /// + /// Gets the type of the configuration page. + /// + /// The type of the configuration page. + public ConfigurationPageType ConfigurationPageType => ConfigurationPageType.PluginConfiguration; + + public IPlugin Plugin => Trakt.Plugin.Instance; + } +} diff --git a/Trakt/Configuration/configPage.html b/Trakt/Configuration/configPage.html new file mode 100644 index 0000000..79afbdf --- /dev/null +++ b/Trakt/Configuration/configPage.html @@ -0,0 +1,214 @@ + + + + Trakt + + + +
+
+
+ +
+
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • +
    +

    Locations Excluded:

    +
    +
    +
    +
  • +
  • + + +
    + By default, Import from Trakt task will overwrite any watched status if item is marked + as unwached on Trakt. When enabled only watched status will be imported. +
    +
  • +
  • + + +
    + By default, watched status will be synchronized. This changes it so that emby just copies the status of trakt, and the only time data watched status is changed on trakt is when a video just finished playing (just scrobbles, no history changes). +
    +
  • +
  • + + +
    + When enabled, all data sent to trakt is logged. +
    +
  • +
  • + + +
    + Send audio and video metadata to Trakt. +
    +
  • +
  • + + +
  • +
+
+
+
+ + + + + +
+ + + \ No newline at end of file diff --git a/Trakt/Enums.cs b/Trakt/Enums.cs new file mode 100644 index 0000000..09ce87e --- /dev/null +++ b/Trakt/Enums.cs @@ -0,0 +1,9 @@ +namespace Trakt +{ + public enum MediaStatus + { + Watching, + Paused, + Stop + } +} \ No newline at end of file diff --git a/Trakt/Extensions.cs b/Trakt/Extensions.cs new file mode 100644 index 0000000..fb0b5aa --- /dev/null +++ b/Trakt/Extensions.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using Trakt.Api.DataContracts.Users.Collection; + +namespace Trakt +{ + public static class Extensions + { + // Trakt.tv uses Unix timestamps, which are seconds past epoch. + public static DateTime ConvertEpochToDateTime(this long unixTimeStamp) + { + var dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0); + + dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime(); + + return dtDateTime; + } + + + + public static long ConvertToUnixTimeStamp(this DateTime dateTime) + { + try + { + var dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0); + + var ts = dateTime.Subtract(dtDateTime); + + return Convert.ToInt64(ts.TotalSeconds); + } + catch + { + return 0; + } + } + + public static int? ConvertToInt(this string input) + { + int result; + if (int.TryParse(input, out result)) + { + return result; + } + return null; + } + + public static bool IsEmpty(this TraktMetadata metadata) + { + return string.IsNullOrEmpty(metadata.MediaType) && + string.IsNullOrEmpty(metadata.Resolution) && + string.IsNullOrEmpty(metadata.Audio) && + string.IsNullOrEmpty(metadata.AudioChannels); + } + + public static string GetCodecRepresetation(this MediaStream audioStream) + { + var audio = audioStream != null && !string.IsNullOrEmpty(audioStream.Codec) + ? audioStream.Codec.ToLower().Replace(" ", "_") + : null; + switch (audio) + { + case "truehd": + return TraktAudio.dolby_truehd.ToString(); + case "dts": + case "dca": + return TraktAudio.dts.ToString(); + case "dtshd": + return TraktAudio.dts_ma.ToString(); + case "ac3": + return TraktAudio.dolby_digital.ToString(); + case "aac": + return TraktAudio.aac.ToString(); + case "mp2": + return TraktAudio.mp3.ToString(); + case "pcm": + return TraktAudio.lpcm.ToString(); + case "ogg": + return TraktAudio.ogg.ToString(); + case "wma": + return TraktAudio.wma.ToString(); + case "flac": + return TraktAudio.flac.ToString(); + default: + return null; + } + } + + public static bool MetadataIsDifferent(this TraktMovieCollected collectedMovie, Movie movie) + { + var audioStream = movie.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); + + var resolution = movie.GetDefaultVideoStream().GetResolution(); + var audio = GetCodecRepresetation(audioStream); + var audioChannels = audioStream.GetAudioChannels(); + + if (collectedMovie.Metadata == null || collectedMovie.Metadata.IsEmpty()) + { + return !string.IsNullOrEmpty(resolution) || !string.IsNullOrEmpty(audio) || !string.IsNullOrEmpty(audioChannels); + } + return collectedMovie.Metadata.Audio != audio || + collectedMovie.Metadata.AudioChannels != audioChannels || + collectedMovie.Metadata.Resolution != resolution; + } + + public static string GetResolution(this MediaStream videoStream) + { + if (videoStream == null) + { + return null; + } + if (!videoStream.Width.HasValue) + { + return null; + } + if (videoStream.Width.Value >= 3800) + { + return "uhd_4k"; + } + if (videoStream.Width.Value >= 1900) + { + return "hd_1080p"; + } + if (videoStream.Width.Value >= 1270) + { + return "hd_720p"; + } + if (videoStream.Width.Value >= 700) + { + return "sd_480p"; + } + return null; + } + + public static string ToISO8601(this DateTime dt, double hourShift = 0) + { + return dt.AddHours(hourShift).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"); + } + + + public static int GetSeasonNumber(this Episode episode) + { + return (episode.ParentIndexNumber != 0 ? episode.ParentIndexNumber ?? 1 + (episode.Series.AnimeSeriesIndex ?? 1) - 1 : episode.ParentIndexNumber).Value; + } + + public static string GetAudioChannels(this MediaStream audioStream) + { + if (audioStream == null || string.IsNullOrEmpty(audioStream.ChannelLayout)) + { + return null; + } + var channels = audioStream.ChannelLayout.Split('(')[0]; + switch (channels) + { + case "7": + return "6.1"; + case "6": + return "5.1"; + case "5": + return "5.0"; + case "4": + return "4.0"; + case "3": + return "2.1"; + case "stereo": + return "2.0"; + case "mono": + return "1.0"; + default: + return channels; + } + } + + public static IList> ToChunks(this IEnumerable enumerable, int chunkSize) + { + var itemsReturned = 0; + var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable. + var count = list.Count; + var chunks = new List>(); + while (itemsReturned < count) + { + chunks.Add(list.Take(chunkSize).ToList()); + list = list.Skip(chunkSize).ToList(); + itemsReturned += chunkSize; + } + return chunks; + } + + public enum TraktAudio + { + lpcm, + mp3, + aac, + dts, + dts_ma, + flac, + ogg, + wma, + dolby_prologic, + dolby_digital, + dolby_digital_plus, + dolby_truehd + } + } +} diff --git a/Trakt/Helpers/LibraryManagerEventsHelper.cs b/Trakt/Helpers/LibraryManagerEventsHelper.cs new file mode 100644 index 0000000..2e16a69 --- /dev/null +++ b/Trakt/Helpers/LibraryManagerEventsHelper.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using Trakt.Api; +using Trakt.Model; +using Timer = System.Timers.Timer; + +namespace Trakt.Helpers +{ + internal class LibraryManagerEventsHelper + { + private readonly List _queuedEvents; + private Timer _queueTimer; + private readonly ILogger _logger ; + private readonly TraktApi _traktApi; + + /// + /// + /// + /// + /// + public LibraryManagerEventsHelper(ILogger logger, TraktApi traktApi) + { + _queuedEvents = new List(); + _logger = logger; + _traktApi = traktApi; + } + + /// + /// + /// + /// + /// + public void QueueItem(BaseItem item, EventType eventType) + { + if (item == null) + throw new ArgumentNullException("item"); + + if (_queueTimer == null) + { + _queueTimer = new Timer(20000); // fire every 20 seconds + _queueTimer.Elapsed += QueueTimerElapsed; + } + else if (_queueTimer.Enabled) + { + // If enabled then multiple LibraryManager events are firing. Restart the timer + _queueTimer.Stop(); + _queueTimer.Start(); + } + + if (!_queueTimer.Enabled) + { + _queueTimer.Enabled = true; + } + + + var users = Plugin.Instance.PluginConfiguration.TraktUsers; + + if (users == null || users.Length == 0) return; + + // we need to process the video for each user + foreach (var user in users.Where(x => _traktApi.CanSync(item, x))) + { + // we have a match, this user is watching the folder the video is in. Add to queue and they + // will be processed when the next timer elapsed event fires. + var libraryEvent = new LibraryEvent {Item = item, TraktUser = user, EventType = eventType}; + _queuedEvents.Add(libraryEvent); + } + + } + + /// + /// + /// + /// + /// + private void QueueTimerElapsed(object sender, ElapsedEventArgs e) + { + _logger.Info("Timer elapsed - Processing queued items"); + + if (!_queuedEvents.Any()) + { + _logger.Info("No events... Stopping queue timer"); + // This may need to go + _queueTimer.Enabled = false; + return; + } + var queue = _queuedEvents.ToList(); + _queuedEvents.Clear(); + foreach (var traktUser in Plugin.Instance.PluginConfiguration.TraktUsers) + { + var queuedMovieDeletes = queue.Where(ev => + new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) && + ev.Item is Movie && + ev.EventType == EventType.Remove).ToList(); + + if (queuedMovieDeletes.Any()) + { + _logger.Info(queuedMovieDeletes.Count + " Movie Deletes to Process"); + ProcessQueuedMovieEvents(queuedMovieDeletes, traktUser, EventType.Remove); + } + else + { + _logger.Info("No Movie Deletes to Process"); + } + + var queuedMovieAdds = queue.Where(ev => + new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) && + ev.Item is Movie && + ev.EventType == EventType.Add).ToList(); + + if (queuedMovieAdds.Any()) + { + _logger.Info(queuedMovieAdds.Count + " Movie Adds to Process"); + ProcessQueuedMovieEvents(queuedMovieAdds, traktUser, EventType.Add); + } + else + { + _logger.Info("No Movie Adds to Process"); + } + + var queuedEpisodeDeletes = queue.Where(ev => + new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) && + ev.Item is Episode && + ev.EventType == EventType.Remove).ToList(); + + if (queuedEpisodeDeletes.Any()) + { + _logger.Info(queuedEpisodeDeletes.Count + " Episode Deletes to Process"); + ProcessQueuedEpisodeEvents(queuedEpisodeDeletes, traktUser, EventType.Remove); + } + else + { + _logger.Info("No Episode Deletes to Process"); + } + + var queuedEpisodeAdds = queue.Where(ev => + new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) && + ev.Item is Episode && + ev.EventType == EventType.Add).ToList(); + + if (queuedEpisodeAdds.Any()) + { + _logger.Info(queuedEpisodeAdds.Count + " Episode Adds to Process"); + ProcessQueuedEpisodeEvents(queuedEpisodeAdds, traktUser, EventType.Add); + } + else + { + _logger.Info("No Episode Adds to Process"); + } + + var queuedShowDeletes = queue.Where(ev => + new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) && + ev.Item is Series && + ev.EventType == EventType.Remove).ToList(); + + if (queuedShowDeletes.Any()) + { + _logger.Info(queuedMovieDeletes.Count + " Series Deletes to Process"); + ProcessQueuedShowEvents(queuedShowDeletes, traktUser, EventType.Remove); + } + else + { + _logger.Info("No Series Deletes to Process"); + } + } + + // Everything is processed. Reset the event list. + _queueTimer.Enabled = false; + _queuedEvents.Clear(); + + } + + private async Task ProcessQueuedShowEvents(IEnumerable events, TraktUser traktUser, EventType eventType) + { + var shows = events.Select(lev => (Series)lev.Item) + .Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProviders.Tvdb))) + .ToList(); + try + { + // Should probably not be awaiting this, but it's unlikely a user will be deleting more than one or two shows at a time + foreach (var show in shows) + await _traktApi.SendLibraryUpdateAsync(show, traktUser, CancellationToken.None, eventType); + } + catch (Exception ex) + { + _logger.ErrorException("Exception handled processing queued series events", ex); + } + } + + /// + /// + /// + /// + /// + /// + /// + private async Task ProcessQueuedMovieEvents(IEnumerable events, TraktUser traktUser, EventType eventType) + { + var movies = events.Select(lev => (Movie) lev.Item) + .Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProviders.Imdb))) + .ToList(); + try + { + await _traktApi.SendLibraryUpdateAsync(movies, traktUser, CancellationToken.None, eventType); + } + catch (Exception ex) + { + _logger.ErrorException("Exception handled processing queued movie events", ex); + } + + } + + /// + /// + /// + /// + /// + /// + /// + private async Task ProcessQueuedEpisodeEvents(IEnumerable events, TraktUser traktUser, EventType eventType) + { + var episodes = events.Select(lev => (Episode) lev.Item) + .Where(lev => lev.Series != null && (!string.IsNullOrEmpty(lev.Series.Name) && !string.IsNullOrEmpty(lev.Series.GetProviderId(MetadataProviders.Tvdb)))) + .OrderBy(i => i.Series.Id) + .ToList(); + + // Can't progress further without episodes + if (!episodes.Any()) + { + _logger.Info("episodes count is 0"); + + return; + } + + var payload = new List(); + var currentSeriesId = episodes[0].Series.Id; + + foreach (var ep in episodes) + { + if (!currentSeriesId.Equals(ep.Series.Id)) + { + // We're starting a new series. Time to send the current one to trakt.tv + await _traktApi.SendLibraryUpdateAsync(payload, traktUser, CancellationToken.None, eventType); + + currentSeriesId = ep.Series.Id; + payload.Clear(); + } + + payload.Add(ep); + } + + if (payload.Any()) + { + try + { + await _traktApi.SendLibraryUpdateAsync(payload, traktUser, CancellationToken.None, eventType); + } + catch (Exception ex) + { + _logger.ErrorException("Exception handled processing queued episode events", ex); + } + } + } + } + + #region internal helper types + + internal class LibraryEvent + { + public BaseItem Item { get; set; } + public TraktUser TraktUser { get; set; } + public EventType EventType { get; set; } + } + + public enum EventType + { + Add, + Remove, + Update + } + + #endregion +} diff --git a/Trakt/Helpers/UserDataManagerEventsHelper.cs b/Trakt/Helpers/UserDataManagerEventsHelper.cs new file mode 100644 index 0000000..2c312e1 --- /dev/null +++ b/Trakt/Helpers/UserDataManagerEventsHelper.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Timers; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using Trakt.Api; +using Trakt.Model; +using Timer = System.Timers.Timer; + +namespace Trakt.Helpers +{ + /// + /// Helper class used to update the watched status of movies/episodes. Attempts to organise + /// requests to lower trakt.tv api calls. + /// + internal class UserDataManagerEventsHelper + { + private List _userDataPackages; + private readonly ILogger _logger; + private readonly TraktApi _traktApi; + private Timer _timer; + + + /// + /// + /// + /// + /// + public UserDataManagerEventsHelper(ILogger logger, TraktApi traktApi) + { + _userDataPackages = new List(); + _logger = logger; + _traktApi = traktApi; + } + + + + /// + /// + /// + /// + /// + public void ProcessUserDataSaveEventArgs(UserDataSaveEventArgs userDataSaveEventArgs, TraktUser traktUser) + { + var userPackage = _userDataPackages.FirstOrDefault(e => e.TraktUser.Equals(traktUser)); + + if (userPackage == null) + { + userPackage = new UserDataPackage { TraktUser = traktUser }; + _userDataPackages.Add(userPackage); + } + + + if (_timer == null) + { + _timer = new Timer(5000); + _timer.Elapsed += TimerElapsed; + } + + if (_timer.Enabled) + { + _timer.Stop(); + _timer.Start(); + } + else + { + _timer.Start(); + } + + var movie = userDataSaveEventArgs.Item as Movie; + + if (movie != null) + { + if (userDataSaveEventArgs.UserData.Played) + { + userPackage.SeenMovies.Add(movie); + + if (userPackage.SeenMovies.Count >= 100) + { + _traktApi.SendMoviePlaystateUpdates(userPackage.SeenMovies, userPackage.TraktUser, true, + CancellationToken.None).ConfigureAwait(false); + userPackage.SeenMovies = new List(); + } + } + else + { + userPackage.UnSeenMovies.Add(movie); + + if (userPackage.UnSeenMovies.Count >= 100) + { + _traktApi.SendMoviePlaystateUpdates(userPackage.UnSeenMovies, userPackage.TraktUser, false, + CancellationToken.None).ConfigureAwait(false); + userPackage.UnSeenMovies = new List(); + } + } + + return; + } + + var episode = userDataSaveEventArgs.Item as Episode; + + if (episode == null) return; + + // If it's not the series we're currently storing, upload our episodes and reset the arrays + if (!userPackage.CurrentSeriesId.Equals(episode.Series.Id)) + { + if (userPackage.SeenEpisodes.Any()) + { + _traktApi.SendEpisodePlaystateUpdates(userPackage.SeenEpisodes, userPackage.TraktUser, true, + CancellationToken.None).ConfigureAwait(false); + userPackage.SeenEpisodes = new List(); + } + + if (userPackage.UnSeenEpisodes.Any()) + { + _traktApi.SendEpisodePlaystateUpdates(userPackage.UnSeenEpisodes, userPackage.TraktUser, false, + CancellationToken.None).ConfigureAwait(false); + userPackage.SeenEpisodes = new List(); + } + + userPackage.CurrentSeriesId = episode.Series.Id; + } + + if (userDataSaveEventArgs.UserData.Played) + { + userPackage.SeenEpisodes.Add(episode); + } + else + { + userPackage.UnSeenEpisodes.Add(episode); + } + } + + + + /// + /// + /// + /// + /// + void TimerElapsed(object sender, ElapsedEventArgs e) + { + _timer.Enabled = false; + + foreach (var package in _userDataPackages) + { + + if (package.UnSeenMovies.Any()) + { + var movies = package.UnSeenMovies.ToList(); + package.UnSeenMovies.Clear(); + _traktApi.SendMoviePlaystateUpdates(movies, package.TraktUser, false, + CancellationToken.None).ConfigureAwait(false); + } + if (package.SeenMovies.Any()) + { + var movies = package.SeenMovies.ToList(); + package.SeenMovies.Clear(); + _traktApi.SendMoviePlaystateUpdates(movies, package.TraktUser, true, + CancellationToken.None).ConfigureAwait(false); + } + if (package.UnSeenEpisodes.Any()) + { + var episodes = package.UnSeenEpisodes.ToList(); + package.UnSeenEpisodes.Clear(); + _traktApi.SendEpisodePlaystateUpdates(episodes, package.TraktUser, false, + CancellationToken.None).ConfigureAwait(false); + } + if (package.SeenEpisodes.Any()) + { + var episodes = package.SeenEpisodes.ToList(); + package.SeenEpisodes.Clear(); + _traktApi.SendEpisodePlaystateUpdates(episodes, package.TraktUser, true, + CancellationToken.None).ConfigureAwait(false); + } + } + } + } + + + + /// + /// Class that contains all the items to be reported to trakt.tv and supporting properties. + /// + internal class UserDataPackage + { + public TraktUser TraktUser; + public Guid CurrentSeriesId; + public List SeenMovies; + public List UnSeenMovies; + public List SeenEpisodes; + public List UnSeenEpisodes; + + public UserDataPackage() + { + SeenMovies = new List(); + UnSeenMovies = new List(); + SeenEpisodes = new List(); + UnSeenEpisodes = new List(); + } + } +} diff --git a/Trakt/Helpers/UserHelpers.cs b/Trakt/Helpers/UserHelpers.cs new file mode 100644 index 0000000..c5609db --- /dev/null +++ b/Trakt/Helpers/UserHelpers.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using MediaBrowser.Controller.Entities; +using Trakt.Model; + +namespace Trakt.Helpers +{ + internal static class UserHelper + { + public static TraktUser GetTraktUser(User user) + { + return Plugin.Instance.PluginConfiguration.TraktUsers != null ? Plugin.Instance.PluginConfiguration.TraktUsers.FirstOrDefault(tUser => new Guid(tUser.LinkedMbUserId).Equals(user.Id)) : null; + } + + public static TraktUser GetTraktUser(string userId) + { + var userGuid = new Guid(userId); + return Plugin.Instance.PluginConfiguration.TraktUsers != null ? Plugin.Instance.PluginConfiguration.TraktUsers.FirstOrDefault(tUser => new Guid(tUser.LinkedMbUserId).Equals(userGuid)) : null; + } + } +} diff --git a/Trakt/Model/TraktUser.cs b/Trakt/Model/TraktUser.cs new file mode 100644 index 0000000..93eb580 --- /dev/null +++ b/Trakt/Model/TraktUser.cs @@ -0,0 +1,27 @@ +using System; + +namespace Trakt.Model +{ + public class TraktUser + { + public String UserName { get; set; } + + public String Password { get; set; } + + public String LinkedMbUserId { get; set; } + + public bool UsesAdvancedRating { get; set; } + + public String UserToken { get; set; } + + public bool SkipUnwatchedImportFromTrakt { get; set; } + + public bool PostWatchedHistory { get; set; } + + public bool ExtraLogging { get; set; } + + public bool ExportMediaInfo { get; set; } + + public String[] LocationsExcluded { get; set; } + } +} diff --git a/Trakt/Plugin.cs b/Trakt/Plugin.cs new file mode 100644 index 0000000..f3b204b --- /dev/null +++ b/Trakt/Plugin.cs @@ -0,0 +1,29 @@ +using System.Threading; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Serialization; +using Trakt.Configuration; + +namespace Trakt +{ + public class Plugin : BasePlugin + { + public SemaphoreSlim TraktResourcePool = new SemaphoreSlim(2, 2); + + public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) + : base(appPaths, xmlSerializer) + { + Instance = this; + } + + public override string Name => "Trakt"; + + + public override string Description + => "Watch, rate and discover media using Trakt. The htpc just got more social"; + + public static Plugin Instance { get; private set; } + + public PluginConfiguration PluginConfiguration => Configuration; + } +} diff --git a/Trakt/Properties/AssemblyInfo.cs b/Trakt/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7a3df70 --- /dev/null +++ b/Trakt/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Trakt")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Trakt")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8abc6789-fde2-4705-8592-4028806fa343")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// \ No newline at end of file diff --git a/Trakt/ScheduledTasks/SyncFromTraktTask.cs b/Trakt/ScheduledTasks/SyncFromTraktTask.cs new file mode 100644 index 0000000..b1854af --- /dev/null +++ b/Trakt/ScheduledTasks/SyncFromTraktTask.cs @@ -0,0 +1,400 @@ +namespace Trakt.ScheduledTasks +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + using CommonIO; + + using MediaBrowser.Common.Net; + using MediaBrowser.Common.ScheduledTasks; + using MediaBrowser.Controller; + using MediaBrowser.Controller.Entities; + using MediaBrowser.Controller.Entities.Movies; + using MediaBrowser.Controller.Entities.TV; + using MediaBrowser.Controller.Library; + using MediaBrowser.Model.Entities; + using MediaBrowser.Model.Logging; + using MediaBrowser.Model.Serialization; + + using Trakt.Api; + using Trakt.Api.DataContracts.BaseModel; + using Trakt.Api.DataContracts.Users.Collection; + using Trakt.Api.DataContracts.Users.Watched; + using Trakt.Helpers; + + /// + /// Task that will Sync each users trakt.tv profile with their local library. This task will only include + /// watched states. + /// + class SyncFromTraktTask : IScheduledTask + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly TraktApi _traktApi; + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SyncFromTraktTask(ILogManager logger, IJsonSerializer jsonSerializer, IUserManager userManager, IUserDataManager userDataManager, IHttpClient httpClient, IServerApplicationHost appHost, IFileSystem fileSystem, ILibraryManager libraryManager) + { + _userManager = userManager; + _userDataManager = userDataManager; + _libraryManager = libraryManager; + _logger = logger.GetLogger("Trakt"); + _traktApi = new TraktApi(jsonSerializer, _logger, httpClient, appHost, userDataManager, fileSystem); + } + + /// + /// + /// + /// + /// + /// + public async Task Execute(CancellationToken cancellationToken, IProgress progress) + { + var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList(); + + // No point going further if we don't have users. + if (users.Count == 0) + { + _logger.Info("No Users returned"); + return; + } + + // purely for progress reporting + var percentPerUser = 100 / users.Count; + double currentProgress = 0; + var numComplete = 0; + + foreach (var user in users) + { + try + { + await SyncTraktDataForUser(user, currentProgress, cancellationToken, progress, percentPerUser).ConfigureAwait(false); + + numComplete++; + currentProgress = percentPerUser * numComplete; + progress.Report(currentProgress); + } + catch (Exception ex) + { + _logger.ErrorException("Error syncing trakt data for user {0}", ex, user.Name); + } + } + } + + private async Task SyncTraktDataForUser(User user, double currentProgress, CancellationToken cancellationToken, IProgress progress, double percentPerUser) + { + var libraryRoot = user.RootFolder; + var traktUser = UserHelper.GetTraktUser(user); + + IEnumerable traktWatchedMovies; + IEnumerable traktWatchedShows; + + try + { + /* + * In order to be as accurate as possible. We need to download the users show collection & the users watched shows. + * It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched + * like they do for movies. + */ + traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false); + traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Exception handled", ex); + throw; + } + + _logger.Info("Trakt.tv watched Movies count = " + traktWatchedMovies.Count()); + _logger.Info("Trakt.tv watched Shows count = " + traktWatchedShows.Count()); + + var mediaItems = + _libraryManager.GetItemList( + new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Episode).Name }, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }) + .Where(i => _traktApi.CanSync(i, traktUser)) + .OrderBy( + i => + { + var episode = i as Episode; + + return episode != null ? episode.Series.Id : i.Id; + }).ToList(); + + // purely for progress reporting + var percentPerItem = percentPerUser / mediaItems.Count; + + foreach (var movie in mediaItems.OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + var matchedMovie = FindMatch(movie, traktWatchedMovies); + + if (matchedMovie != null) + { + _logger.Debug("Movie is in Watched list " + movie.Name); + + var userData = _userDataManager.GetUserData(user.Id, movie); + bool changed = false; + + // set movie as watched + if (!userData.Played) + { + userData.Played = true; + userData.LastPlayedDate = DateTime.UtcNow; + changed = true; + } + + // keep the highest play count + int playcount = Math.Max(matchedMovie.Plays, userData.PlayCount); + + // set movie playcount + if (userData.PlayCount != playcount) + { + userData.PlayCount = playcount; + changed = true; + } + + // Set last played to whichever is most recent, remote or local time... + if (!string.IsNullOrEmpty(matchedMovie.LastWatchedAt)) + { + var tLastPlayed = DateTime.Parse(matchedMovie.LastWatchedAt); + var latestPlayed = tLastPlayed > userData.LastPlayedDate ? tLastPlayed : userData.LastPlayedDate; + if (userData.LastPlayedDate != latestPlayed) + { + userData.LastPlayedDate = latestPlayed; + changed = true; + } + } + + // Only process if there's a change + if (changed) + { + await + _userDataManager.SaveUserData( + user.Id, + movie, + userData, + UserDataSaveReason.Import, + cancellationToken); + } + } + else + { + _logger.Info("Failed to match " + movie.Name); + } + + // purely for progress reporting + currentProgress += percentPerItem; + progress.Report(currentProgress); + } + + foreach (var episode in mediaItems.OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + var matchedShow = FindMatch(episode.Series, traktWatchedShows); + + if (matchedShow != null) + { + var matchedSeason = + matchedShow.Seasons.FirstOrDefault( + tSeason => + tSeason.Number + == (episode.ParentIndexNumber == 0 + ? 0 + : ((episode.ParentIndexNumber ?? 1) + (episode.Series.AnimeSeriesIndex ?? 1) - 1))); + + // if it's not a match then it means trakt doesn't know about the season, leave the watched state alone and move on + if (matchedSeason != null) + { + // episode is in users libary. Now we need to determine if it's watched + var userData = _userDataManager.GetUserData(user.Id, episode); + bool changed = false; + + var matchedEpisode = + matchedSeason.Episodes.FirstOrDefault(x => x.Number == (episode.IndexNumber ?? -1)); + + if (matchedEpisode != null) + { + _logger.Debug("Episode is in Watched list " + GetVerboseEpisodeData(episode)); + + // Set episode as watched + if (!userData.Played) + { + userData.Played = true; + userData.LastPlayedDate = DateTime.UtcNow; + changed = true; + } + + // keep the highest play count + int playcount = Math.Max(matchedEpisode.Plays, userData.PlayCount); + + // set episode playcount + if (userData.PlayCount != playcount) + { + userData.PlayCount = playcount; + changed = true; + } + } + else if (!traktUser.SkipUnwatchedImportFromTrakt) + { + userData.Played = false; + userData.PlayCount = 0; + userData.LastPlayedDate = null; + changed = true; + } + + // only process if changed + if (changed) + { + await + _userDataManager.SaveUserData( + user.Id, + episode, + userData, + UserDataSaveReason.Import, + cancellationToken); + } + } + else + { + _logger.Debug("No Season match in Watched shows list " + GetVerboseEpisodeData(episode)); + } + } + else + { + _logger.Debug("No Show match in Watched shows list " + GetVerboseEpisodeData(episode)); + } + + // purely for progress reporting + currentProgress += percentPerItem; + progress.Report(currentProgress); + } + + // _logger.Info(syncItemFailures + " items not parsed"); + } + + private string GetVerboseEpisodeData(Episode episode) + { + string episodeString = string.Empty; + episodeString += "Episode: " + (episode.ParentIndexNumber != null ? episode.ParentIndexNumber.ToString() : "null"); + episodeString += "x" + (episode.IndexNumber != null ? episode.IndexNumber.ToString() : "null"); + episodeString += " '" + episode.Name + "' "; + episodeString += "Series: '" + (episode.Series != null + ? !string.IsNullOrWhiteSpace(episode.Series.Name) + ? episode.Series.Name + : "null property" + : "null class"); + episodeString += "'"; + + return episodeString; + } + + public static TraktShowWatched FindMatch(Series item, IEnumerable results) + { + return results.FirstOrDefault(i => IsMatch(item, i.Show)); + } + + public static TraktShowCollected FindMatch(Series item, IEnumerable results) + { + return results.FirstOrDefault(i => IsMatch(item, i.Show)); + } + + public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable results) + { + return results.FirstOrDefault(i => IsMatch(item, i.Movie)); + } + + public static IEnumerable FindMatches(BaseItem item, IEnumerable results) + { + return results.Where(i => IsMatch(item, i.Movie)).ToList(); + } + + public static bool IsMatch(BaseItem item, TraktMovie movie) + { + var imdb = item.GetProviderId(MetadataProviders.Imdb); + + if (!string.IsNullOrWhiteSpace(imdb) && + string.Equals(imdb, movie.Ids.Imdb, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var tmdb = item.GetProviderId(MetadataProviders.Tmdb); + + if (movie.Ids.Tmdb.HasValue && string.Equals(tmdb, movie.Ids.Tmdb.Value.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (item.Name == movie.Title && item.ProductionYear == movie.Year) + { + return true; + } + + return false; + } + + public static bool IsMatch(Series item, TraktShow show) + { + var tvdb = item.GetProviderId(MetadataProviders.Tvdb); + if (!string.IsNullOrWhiteSpace(tvdb) && + string.Equals(tvdb, show.Ids.Tvdb.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var imdb = item.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrWhiteSpace(imdb) && + string.Equals(imdb, show.Ids.Imdb, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + + + /// + /// + /// + /// + public IEnumerable GetDefaultTriggers() + { + return new List(); + } + + /// + /// + /// + public string Name => "Import playstates from Trakt.tv"; + + /// + /// + /// + public string Description => "Sync Watched/Unwatched status from Trakt.tv for each MB3 user that has a configured Trakt account"; + + /// + /// + /// + public string Category => "Trakt"; + } +} \ No newline at end of file diff --git a/Trakt/ScheduledTasks/SyncLibraryTask.cs b/Trakt/ScheduledTasks/SyncLibraryTask.cs new file mode 100644 index 0000000..1c7912d --- /dev/null +++ b/Trakt/ScheduledTasks/SyncLibraryTask.cs @@ -0,0 +1,501 @@ +namespace Trakt.ScheduledTasks +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + using CommonIO; + + using MediaBrowser.Common.Net; + using MediaBrowser.Common.ScheduledTasks; + using MediaBrowser.Controller; + using MediaBrowser.Controller.Entities; + using MediaBrowser.Controller.Entities.Movies; + using MediaBrowser.Controller.Entities.TV; + using MediaBrowser.Controller.Library; + using MediaBrowser.Model.Entities; + using MediaBrowser.Model.Logging; + using MediaBrowser.Model.Serialization; + + using Trakt.Api; + using Trakt.Api.DataContracts.Sync; + using Trakt.Helpers; + using Trakt.Model; + + /// + /// Task that will Sync each users local library with their respective trakt.tv profiles. This task will only include + /// titles, watched states will be synced in other tasks. + /// + public class SyncLibraryTask : IScheduledTask + { + //private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + private readonly ILogger _logger; + private readonly TraktApi _traktApi; + private readonly IUserDataManager _userDataManager; + private readonly ILibraryManager _libraryManager; + + public SyncLibraryTask(ILogManager logger, IJsonSerializer jsonSerializer, IUserManager userManager, IUserDataManager userDataManager, IHttpClient httpClient, IServerApplicationHost appHost, IFileSystem fileSystem, ILibraryManager libraryManager) + { + _jsonSerializer = jsonSerializer; + _userManager = userManager; + _userDataManager = userDataManager; + _libraryManager = libraryManager; + _logger = logger.GetLogger("Trakt"); + _traktApi = new TraktApi(jsonSerializer, _logger, httpClient, appHost, userDataManager, fileSystem); + } + + public IEnumerable GetDefaultTriggers() + { + return new List(); + } + + public async Task Execute(CancellationToken cancellationToken, IProgress progress) + { + var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList(); + + // No point going further if we don't have users. + if (users.Count == 0) + { + _logger.Info("No Users returned"); + return; + } + + // purely for progress reporting + var progPercent = 0.0; + var percentPerUser = 100 / users.Count; + + foreach (var user in users) + { + var traktUser = UserHelper.GetTraktUser(user); + + // I'll leave this in here for now, but in reality this continue should never be reached. + if (string.IsNullOrEmpty(traktUser?.LinkedMbUserId)) + { + _logger.Error("traktUser is either null or has no linked MB account"); + continue; + } + + await SyncUserLibrary(user, traktUser, progPercent, percentPerUser, progress, cancellationToken) + .ConfigureAwait(false); + + progPercent += percentPerUser; + } + } + + private async Task SyncUserLibrary( + User user, + TraktUser traktUser, + double progPercent, + double percentPerUser, + IProgress progress, + CancellationToken cancellationToken) + { + // purely for progress reporting + var mediaItemsCount = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Episode).Name }, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }) + .Count(i => _traktApi.CanSync(i, traktUser)); + + if (mediaItemsCount == 0) + { + _logger.Info("No media found for '" + user.Name + "'."); + return; + } + + _logger.Info(mediaItemsCount + " Items found for '" + user.Name + "'."); + + var percentPerItem = (float)percentPerUser / mediaItemsCount / 2.0; + + await SyncMovies(user, traktUser, progress, progPercent, percentPerItem, cancellationToken); + await SyncShows(user, traktUser, progress, progPercent, percentPerItem, cancellationToken); + } + + /// + /// Sync watched and collected status of s with trakt. + /// + /// + /// to get (e.g. watched status) from. + /// + /// + /// The to sync with. + /// + /// + /// Progress reporter. + /// + /// + /// Initial progress value. + /// + /// + /// Progress percent per item. + /// + /// + /// The cancellation token. + /// + /// + /// Awaitable . + /// + private async Task SyncMovies( + User user, + TraktUser traktUser, + IProgress progress, + double progPercent, + double percentPerItem, + CancellationToken cancellationToken) + { + /* + * In order to sync watched status to trakt.tv we need to know what's been watched on Trakt already. This + * will stop us from endlessly incrementing the watched values on the site. + */ + var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false); + var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser).ConfigureAwait(false); + var libraryMovies = + _libraryManager.GetItemList( + new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Movie).Name }, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }) + .Where(x => _traktApi.CanSync(x, traktUser)) + .OrderBy(x => x.Name) + .ToList(); + var collectedMovies = new List(); + var playedMovies = new List(); + var unplayedMovies = new List(); + + foreach (var child in libraryMovies) + { + cancellationToken.ThrowIfCancellationRequested(); + var libraryMovie = child as Movie; + var userData = _userDataManager.GetUserData(user.Id, child); + + // if movie is not collected, or (export media info setting is enabled and every collected matching movie has different metadata), collect it + var collectedMathingMovies = SyncFromTraktTask.FindMatches(libraryMovie, traktCollectedMovies).ToList(); + if (!collectedMathingMovies.Any() + || (traktUser.ExportMediaInfo + && collectedMathingMovies.All(collectedMovie => collectedMovie.MetadataIsDifferent(libraryMovie)))) + { + collectedMovies.Add(libraryMovie); + } + + var movieWatched = SyncFromTraktTask.FindMatch(libraryMovie, traktWatchedMovies); + + // if the movie has been played locally and is unplayed on trakt.tv then add it to the list + if (userData.Played) + { + if (movieWatched == null) + { + if (traktUser.PostWatchedHistory) + { + playedMovies.Add(libraryMovie); + } + else + { + userData.Played = false; + await + _userDataManager.SaveUserData( + user.Id, + libraryMovie, + userData, + UserDataSaveReason.Import, + cancellationToken); + } + } + } + else + { + // If the show has not been played locally but is played on trakt.tv then add it to the unplayed list + if (movieWatched != null) + { + unplayedMovies.Add(libraryMovie); + } + } + + // purely for progress reporting + progPercent += percentPerItem; + progress.Report(progPercent); + } + + // send movies to mark collected + await SendMovieCollectionUpdates(true, traktUser, collectedMovies, progress, progPercent, percentPerItem, cancellationToken); + + // send movies to mark watched + await SendMoviePlaystateUpdates(true, traktUser, playedMovies, progress, progPercent, percentPerItem, cancellationToken); + + // send movies to mark unwatched + await SendMoviePlaystateUpdates(false, traktUser, unplayedMovies, progress, progPercent, percentPerItem, cancellationToken); + } + + private async Task SendMovieCollectionUpdates( + bool collected, + TraktUser traktUser, + List movies, + IProgress progress, + double progPercent, + double percentPerItem, + CancellationToken cancellationToken) + { + _logger.Info("Movies to " + (collected ? "add to" : "remove from") + " Collection: " + movies.Count); + if (movies.Count > 0) + { + try + { + var dataContracts = + await + _traktApi.SendLibraryUpdateAsync(movies, traktUser, cancellationToken, collected ? EventType.Add : EventType.Remove) + .ConfigureAwait(false); + if (dataContracts != null) + { + foreach (var traktSyncResponse in dataContracts) + { + LogTraktResponseDataContract(traktSyncResponse); + } + } + } + catch (ArgumentNullException argNullEx) + { + _logger.ErrorException("ArgumentNullException handled sending movies to trakt.tv", argNullEx); + } + catch (Exception e) + { + _logger.ErrorException("Exception handled sending movies to trakt.tv", e); + } + + // purely for progress reporting + progPercent += percentPerItem * movies.Count; + progress.Report(progPercent); + } + } + + private async Task SendMoviePlaystateUpdates( + bool seen, + TraktUser traktUser, + List playedMovies, + IProgress progress, + double progPercent, + double percentPerItem, + CancellationToken cancellationToken) + { + _logger.Info("Movies to set " + (seen ? string.Empty : "un") + "watched: " + playedMovies.Count); + if (playedMovies.Count > 0) + { + try + { + var dataContracts = + await _traktApi.SendMoviePlaystateUpdates(playedMovies, traktUser, seen, cancellationToken); + if (dataContracts != null) + { + foreach (var traktSyncResponse in dataContracts) + { + LogTraktResponseDataContract(traktSyncResponse); + } + } + } + catch (Exception e) + { + _logger.ErrorException("Error updating movie play states", e); + } + + // purely for progress reporting + progPercent += percentPerItem * playedMovies.Count; + progress.Report(progPercent); + } + } + + private async Task SyncShows( + User user, + TraktUser traktUser, + IProgress progress, + double progPercent, + double percentPerItem, + CancellationToken cancellationToken) + { + var traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false); + var traktCollectedShows = await _traktApi.SendGetCollectedShowsRequest(traktUser).ConfigureAwait(false); + var episodeItems = + _libraryManager.GetItemList( + new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Episode).Name }, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }) + .Where(x => _traktApi.CanSync(x, traktUser)) + .OrderBy(x => (x as Episode)?.SeriesName) + .ToList(); + + var collectedEpisodes = new List(); + var playedEpisodes = new List(); + var unplayedEpisodes = new List(); + + foreach (var child in episodeItems) + { + cancellationToken.ThrowIfCancellationRequested(); + var episode = child as Episode; + var userData = _userDataManager.GetUserData(user.Id, episode); + var isPlayedTraktTv = false; + var traktWatchedShow = SyncFromTraktTask.FindMatch(episode.Series, traktWatchedShows); + + if (traktWatchedShow?.Seasons != null && traktWatchedShow.Seasons.Count > 0) + { + isPlayedTraktTv = + traktWatchedShow.Seasons.Any( + season => + season.Number == episode.GetSeasonNumber() && season.Episodes != null + && season.Episodes.Any(te => te.Number == episode.IndexNumber && te.Plays > 0)); + } + + // if the show has been played locally and is unplayed on trakt.tv then add it to the list + if (userData != null && userData.Played && !isPlayedTraktTv) + { + if (traktUser.PostWatchedHistory) + { + playedEpisodes.Add(episode); + } + else + { + userData.Played = false; + await + _userDataManager.SaveUserData( + user.Id, + episode, + userData, + UserDataSaveReason.Import, + cancellationToken); + } + } + else if (userData != null && !userData.Played && isPlayedTraktTv) + { + // If the show has not been played locally but is played on trakt.tv then add it to the unplayed list + unplayedEpisodes.Add(episode); + } + + var traktCollectedShow = SyncFromTraktTask.FindMatch(episode.Series, traktCollectedShows); + if (traktCollectedShow?.Seasons == null + || traktCollectedShow.Seasons.All(x => x.Number != episode.ParentIndexNumber) + || traktCollectedShow.Seasons.First(x => x.Number == episode.ParentIndexNumber) + .Episodes.All(e => e.Number != episode.IndexNumber)) + { + collectedEpisodes.Add(episode); + } + + // purely for progress reporting + progPercent += percentPerItem; + progress.Report(progPercent); + } + + await SendEpisodeCollectionUpdates(true, traktUser, collectedEpisodes, progress, progPercent, percentPerItem, cancellationToken); + + await SendEpisodePlaystateUpdates(true, traktUser, playedEpisodes, progress, progPercent, percentPerItem, cancellationToken); + + await SendEpisodePlaystateUpdates(false, traktUser, unplayedEpisodes, progress, progPercent, percentPerItem, cancellationToken); + } + + private async Task SendEpisodePlaystateUpdates( + bool seen, + TraktUser traktUser, + List playedEpisodes, + IProgress progress, + double progPercent, + double percentPerItem, + CancellationToken cancellationToken) + { + _logger.Info("Episodes to set " + (seen ? string.Empty : "un") + "watched: " + playedEpisodes.Count); + if (playedEpisodes.Count > 0) + { + try + { + var dataContracts = + await _traktApi.SendEpisodePlaystateUpdates(playedEpisodes, traktUser, seen, cancellationToken); + dataContracts?.ForEach(LogTraktResponseDataContract); + } + catch (Exception e) + { + _logger.ErrorException("Error updating episode play states", e); + } + + // purely for progress reporting + progPercent += percentPerItem * playedEpisodes.Count; + progress.Report(progPercent); + } + } + + private async Task SendEpisodeCollectionUpdates( + bool collected, + TraktUser traktUser, + List collectedEpisodes, + IProgress progress, + double progPercent, + double percentPerItem, + CancellationToken cancellationToken) + { + _logger.Info("Episodes to add to Collection: " + collectedEpisodes.Count); + if (collectedEpisodes.Count > 0) + { + try + { + var dataContracts = + await + _traktApi.SendLibraryUpdateAsync(collectedEpisodes, traktUser, cancellationToken, collected ? EventType.Add : EventType.Remove) + .ConfigureAwait(false); + if (dataContracts != null) + { + foreach (var traktSyncResponse in dataContracts) + { + LogTraktResponseDataContract(traktSyncResponse); + } + } + } + catch (ArgumentNullException argNullEx) + { + _logger.ErrorException("ArgumentNullException handled sending episodes to trakt.tv", argNullEx); + } + catch (Exception e) + { + _logger.ErrorException("Exception handled sending episodes to trakt.tv", e); + } + + // purely for progress reporting + progPercent += percentPerItem * collectedEpisodes.Count; + progress.Report(progPercent); + } + } + + public string Name => "Sync library to trakt.tv"; + + public string Category => "Trakt"; + + public string Description => "Adds any media that is in each users trakt monitored locations to their trakt.tv profile"; + + private void LogTraktResponseDataContract(TraktSyncResponse dataContract) + { + _logger.Debug("TraktResponse Added Movies: " + dataContract.Added.Movies); + _logger.Debug("TraktResponse Added Shows: " + dataContract.Added.Shows); + _logger.Debug("TraktResponse Added Seasons: " + dataContract.Added.Seasons); + _logger.Debug("TraktResponse Added Episodes: " + dataContract.Added.Episodes); + foreach (var traktMovie in dataContract.NotFound.Movies) + { + _logger.Error("TraktResponse not Found:" + _jsonSerializer.SerializeToString(traktMovie)); + } + + foreach (var traktShow in dataContract.NotFound.Shows) + { + _logger.Error("TraktResponse not Found:" + _jsonSerializer.SerializeToString(traktShow)); + } + + foreach (var traktSeason in dataContract.NotFound.Seasons) + { + _logger.Error("TraktResponse not Found:" + _jsonSerializer.SerializeToString(traktSeason)); + } + + foreach (var traktEpisode in dataContract.NotFound.Episodes) + { + _logger.Error("TraktResponse not Found:" + _jsonSerializer.SerializeToString(traktEpisode)); + } + } + } +} diff --git a/Trakt/ServerMediator.cs b/Trakt/ServerMediator.cs new file mode 100644 index 0000000..53db9b4 --- /dev/null +++ b/Trakt/ServerMediator.cs @@ -0,0 +1,310 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Linq; +using CommonIO; +using Trakt.Api; +using Trakt.Helpers; + +namespace Trakt +{ + /// + /// All communication between the server and the plugins server instance should occur in this class. + /// + public class ServerMediator : IServerEntryPoint + { + private readonly ISessionManager _sessionManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private TraktApi _traktApi; + private TraktUriService _service; + private LibraryManagerEventsHelper _libraryManagerEventsHelper; + private readonly UserDataManagerEventsHelper _userDataManagerEventsHelper; + + public static ServerMediator Instance { get; private set; } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public ServerMediator(IJsonSerializer jsonSerializer, ISessionManager sessionManager, IUserDataManager userDataManager, ILibraryManager libraryManager, ILogManager logger, IHttpClient httpClient, IServerApplicationHost appHost, IFileSystem fileSystem) + { + Instance = this; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _logger = logger.GetLogger("Trakt"); + + _traktApi = new TraktApi(jsonSerializer, _logger, httpClient, appHost, userDataManager, fileSystem); + _service = new TraktUriService(_traktApi, _logger, _libraryManager); + _libraryManagerEventsHelper = new LibraryManagerEventsHelper(_logger, _traktApi); + _userDataManagerEventsHelper = new UserDataManagerEventsHelper(_logger, _traktApi); + + userDataManager.UserDataSaved += _userDataManager_UserDataSaved; + } + + /// + /// + /// + /// + /// + void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e) + { + // ignore change events for any reason other than manually toggling played. + if (e.SaveReason != UserDataSaveReason.TogglePlayed) return; + + var baseItem = e.Item as BaseItem; + + if (baseItem != null) + { + // determine if user has trakt credentials + var traktUser = UserHelper.GetTraktUser(e.UserId.ToString()); + + // Can't progress + if (traktUser == null || !_traktApi.CanSync(baseItem, traktUser)) + return; + + // We have a user and the item is in a trakt monitored location. + _userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(e, traktUser); + } + } + + + + /// + /// + /// + public void Run() + { + _sessionManager.PlaybackStart += KernelPlaybackStart; + _sessionManager.PlaybackStopped += KernelPlaybackStopped; + _libraryManager.ItemAdded += LibraryManagerItemAdded; + _libraryManager.ItemRemoved += LibraryManagerItemRemoved; + } + + + + /// + /// + /// + /// + /// + void LibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) + { + if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series)) return; + if (e.Item.LocationType == LocationType.Virtual) return; + _libraryManagerEventsHelper.QueueItem(e.Item, EventType.Remove); + } + + + + /// + /// + /// + /// + /// + void LibraryManagerItemAdded(object sender, ItemChangeEventArgs e) + { + // Don't do anything if it's not a supported media type + if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series)) return; + if (e.Item.LocationType == LocationType.Virtual) return; + _libraryManagerEventsHelper.QueueItem(e.Item, EventType.Add); + } + + + + /// + /// Let Trakt.tv know the user has started to watch something + /// + /// + /// + private async void KernelPlaybackStart(object sender, PlaybackProgressEventArgs e) + { + try + { + _logger.Info("Playback Started"); + + if (e.Users == null || !e.Users.Any() || e.Item == null) + { + _logger.Error("Event details incomplete. Cannot process current media"); + return; + } + + // Since MB3 is user profile friendly, I'm going to need to do a user lookup every time something starts + var traktUser = UserHelper.GetTraktUser(e.Users.FirstOrDefault()); + + if (traktUser == null) + { + _logger.Info("Could not match user with any stored credentials"); + return; + } + + if (!_traktApi.CanSync(e.Item, traktUser)) + { + return; + } + + _logger.Debug(traktUser.LinkedMbUserId + " appears to be monitoring " + e.Item.Path); + + var video = e.Item as Video; + var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ? + (float)(e.PlaybackPositionTicks??0) / video.RunTimeTicks.Value * 100.0f : 0.0f; + + try + { + if (video is Movie) + { + _logger.Debug("Send movie status update"); + await + _traktApi.SendMovieStatusUpdateAsync(video as Movie, MediaStatus.Watching, traktUser, progressPercent). + ConfigureAwait(false); + } + else if (video is Episode) + { + _logger.Debug("Send episode status update"); + await + _traktApi.SendEpisodeStatusUpdateAsync(video as Episode, MediaStatus.Watching, traktUser, progressPercent). + ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.ErrorException("Exception handled sending status update", ex); + } + + var playEvent = new ProgressEvent + { + UserId = e.Users.First().Id, + ItemId = e.Item.Id, + LastApiAccess = DateTime.UtcNow + }; + } + catch (Exception ex) + { + _logger.ErrorException("Error sending watching status update", ex, null); + } + } + + /// + /// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has + /// completed watching the item. + /// + /// + /// + private async void KernelPlaybackStopped(object sender, PlaybackStopEventArgs e) + { + if (e.Users == null || !e.Users.Any() || e.Item == null) + { + _logger.Error("Event details incomplete. Cannot process current media"); + return; + } + + try + { + _logger.Info("Playback Stopped"); + var traktUser = UserHelper.GetTraktUser(e.Users.FirstOrDefault()); + + if (traktUser == null) + { + _logger.Error("Could not match trakt user"); + return; + } + + if (!_traktApi.CanSync(e.Item, traktUser)) + { + return; + } + + var video = e.Item as Video; + + if (e.PlayedToCompletion) + { + _logger.Info("Item is played. Scrobble"); + + try + { + if (video is Movie) + { + await + _traktApi.SendMovieStatusUpdateAsync(video as Movie, MediaStatus.Stop, traktUser, 100). + ConfigureAwait(false); + } + else if (video is Episode) + { + await + _traktApi.SendEpisodeStatusUpdateAsync(video as Episode, MediaStatus.Stop, traktUser, 100) + .ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.ErrorException("Exception handled sending status update", ex); + } + + } + else + { + var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ? + (float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f; + _logger.Info("Item Not fully played. Tell trakt.tv we are no longer watching but don't scrobble"); + + if (video is Movie) + { + await _traktApi.SendMovieStatusUpdateAsync(video as Movie, MediaStatus.Stop, traktUser, progressPercent); + } + else + { + await _traktApi.SendEpisodeStatusUpdateAsync(video as Episode, MediaStatus.Stop, traktUser, progressPercent); + } + } + + } + catch (Exception ex) + { + _logger.ErrorException("Error sending scrobble", ex, null); + } + } + + /// + /// + /// + public void Dispose() + { + _sessionManager.PlaybackStart -= KernelPlaybackStart; + _sessionManager.PlaybackStopped -= KernelPlaybackStopped; + _libraryManager.ItemAdded -= LibraryManagerItemAdded; + _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; + _service = null; + _traktApi = null; + _libraryManagerEventsHelper = null; + } + } + + + + /// + /// + /// + public class ProgressEvent + { + public Guid UserId; + public Guid ItemId; + public DateTime LastApiAccess; + } +} \ No newline at end of file diff --git a/Trakt/Trakt.csproj b/Trakt/Trakt.csproj new file mode 100644 index 0000000..a125207 --- /dev/null +++ b/Trakt/Trakt.csproj @@ -0,0 +1,157 @@ + + + + + Debug + AnyCPU + {7FFC306B-2680-49C7-8BE0-6358B2A8A409} + Library + Properties + Trakt + Trakt + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll + True + + + ..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll + True + + + ..\packages\MediaBrowser.Common.3.0.662\lib\net45\MediaBrowser.Common.dll + True + + + ..\packages\MediaBrowser.Server.Core.3.0.662\lib\net45\MediaBrowser.Controller.dll + True + + + ..\packages\MediaBrowser.Common.3.0.662\lib\net45\MediaBrowser.Model.dll + True + + + ..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll + + + False + ..\packages\ServiceStack.Interfaces.4.0.35\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll + + + + + + + + + + + + + Properties\SharedVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + xcopy "$(TargetPath)" "$(SolutionDir)\..\Emby.dev\ProgramData-Server\Plugins\" /y + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Trakt/packages.config b/Trakt/packages.config new file mode 100644 index 0000000..834fac8 --- /dev/null +++ b/Trakt/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file