This commit is contained in:
Luke Pulverenti 2016-10-10 16:10:58 -04:00
commit 9bb361922a
68 changed files with 5056 additions and 0 deletions

22
.gitattributes vendored Normal file
View File

@ -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

230
.gitignore vendored Normal file
View File

@ -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

6
.nuget/NuGet.Config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
</configuration>

BIN
.nuget/NuGet.exe Normal file

Binary file not shown.

153
.nuget/NuGet.targets Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
<!-- Enable the restore command to run before builds -->
<RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>
<!-- Property that enables building a package from a project -->
<BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>
<!-- Determines if package restore consent is required to restore packages -->
<RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>
<!-- Download NuGet.exe if it does not already exist -->
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageSources)' == '' ">
<!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
<!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
<!--
<PackageSource Include="https://nuget.org/api/v2/" />
<PackageSource Include="https://my-nuget-source/nuget/" />
-->
</ItemGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
<PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
<PackagesDir>$([System.IO.Path]::Combine($(SolutionDir), "packages"))</PackagesDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
<PackagesConfig>packages.config</PackagesConfig>
<PackagesDir>$(SolutionDir)packages</PackagesDir>
</PropertyGroup>
<PropertyGroup>
<!-- NuGet command -->
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\nuget.exe</NuGetExePath>
<PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>
<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>
<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
<RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
<!-- Commands -->
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -o "$(PackagesDir)"</RestoreCommand>
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols</BuildCommand>
<!-- We need to ensure packages are restored prior to assembly resolve -->
<ResolveReferencesDependsOn Condition="$(RestorePackages) == 'true'">
RestorePackages;
$(ResolveReferencesDependsOn);
</ResolveReferencesDependsOn>
<!-- Make the build depend on restore packages -->
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
$(BuildDependsOn);
BuildPackage;
</BuildDependsOn>
</PropertyGroup>
<Target Name="CheckPrerequisites">
<!-- Raise an error if we're unable to locate nuget.exe -->
<Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
<SetEnvironmentVariable EnvKey="VisualStudioVersion" EnvValue="$(VisualStudioVersion)" Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' " />
<!--
Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
This effectively acts as a lock that makes sure that the download operation will only happen once and all
parallel builds will have to wait for it to complete.
-->
<MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT" />
</Target>
<Target Name="_DownloadNuGet">
<DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
</Target>
<Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(RestoreCommand)"
Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />
<Exec Command="$(RestoreCommand)"
LogStandardErrorAsError="true"
Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
</Target>
<Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(BuildCommand)"
Condition=" '$(OS)' != 'Windows_NT' " />
<Exec Command="$(BuildCommand)"
LogStandardErrorAsError="true"
Condition=" '$(OS)' == 'Windows_NT' " />
</Target>
<UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<OutputFilename ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Net" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
OutputFilename = Path.GetFullPath(OutputFilename);
Log.LogMessage("Downloading latest version of NuGet.exe...");
WebClient webClient = new WebClient();
webClient.DownloadFile("https://nuget.org/nuget.exe", OutputFilename);
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
<UsingTask TaskName="SetEnvironmentVariable" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<EnvKey ParameterType="System.String" Required="true" />
<EnvValue ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
Environment.SetEnvironmentVariable(EnvKey, EnvValue, System.EnvironmentVariableTarget.Process);
}
catch {
}
]]>
</Code>
</Task>
</UsingTask>
</Project>

22
LICENSE.md Normal file
View File

@ -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.

13
README.md Normal file
View File

@ -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)

3
SharedVersion.cs Normal file
View File

@ -0,0 +1,3 @@
using System.Reflection;
[assembly: AssemblyVersion("3.0.*")]

31
Trakt.sln Normal file
View File

@ -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

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<TraktSeasonCollected> Seasons { get; set; }
[DataContract]
public class TraktSeasonCollected
{
[DataMember(Name = "number")]
public int Number { get; set; }
[DataMember(Name = "episodes")]
public List<TraktEpisodeCollected> Episodes { get; set; }
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<TraktSeasonRated> Seasons { get; set; }
public class TraktSeasonRated : TraktRated
{
[DataMember(Name = "number")]
public int? Number { get; set; }
[DataMember(Name = "episodes")]
public List<TraktEpisodeRated> Episodes { get; set; }
}
}
}

View File

@ -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<TMovie, TShow, TEpisode>
{
[DataMember(Name = "movies", EmitDefaultValue = false)]
public List<TMovie> Movies { get; set; }
[DataMember(Name = "shows", EmitDefaultValue = false)]
public List<TShow> Shows { get; set; }
[DataMember(Name = "episodes", EmitDefaultValue = false)]
public List<TEpisode> Episodes { get; set; }
}
[DataContract]
public class TraktSyncRated : TraktSync<TraktMovieRated, TraktShowRated, TraktEpisodeRated>
{
}
[DataContract]
public class TraktSyncWatched : TraktSync<TraktMovieWatched, TraktShowWatched, TraktEpisodeWatched>
{
}
[DataContract]
public class TraktSyncCollected : TraktSync<TraktMovieCollected, TraktShowCollected, TraktEpisodeCollected>
{
}
}

View File

@ -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<TraktMovie> Movies { get; set; }
[DataMember(Name = "shows")]
public List<TraktShow> Shows { get; set; }
[DataMember(Name = "episodes")]
public List<TraktEpisode> Episodes { get; set; }
[DataMember(Name = "seasons")]
public List<TraktSeason> Seasons { get; set; }
[DataMember(Name = "people")]
public List<TraktPerson> People { get; set; }
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<TraktEpisodeWatched> Episodes { get; set; }
}
}

View File

@ -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<TraktSeasonWatched> Seasons { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.Runtime.Serialization;
namespace Trakt.Api.DataContracts
{
[DataContract]
public class TraktUserToken
{
[DataMember(Name = "token")]
public string Token { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<TraktSeasonCollected> Seasons { get; set; }
[DataContract]
public class TraktSeasonCollected
{
[DataMember(Name = "number")]
public int Number { get; set; }
[DataMember(Name = "episodes")]
public List<TraktEpisodeCollected> 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; }
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<Season> Seasons { get; set; }
[DataContract]
public class Season
{
[DataMember(Name = "number")]
public int Number { get; set; }
[DataMember(Name = "episodes")]
public List<Episode> Episodes { get; set; }
[DataContract]
public class Episode
{
[DataMember(Name = "number")]
public int Number { get; set; }
[DataMember(Name = "plays")]
public int Plays { get; set; }
}
}
}
}

View File

@ -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
{
/// <summary>
///
/// </summary>
[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; }
}
/// <summary>
///
/// </summary>
[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; }
}
/// <summary>
///
/// </summary>
[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; }
}
/// <summary>
///
/// </summary>
[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; }
}
/// <summary>
///
/// </summary>
public class TraktUriService : IRestfulService
{
private readonly TraktApi _traktApi;
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TraktUriService"/> class.
/// </summary>
/// <param name="traktApi">The trakt API.</param>
/// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param>
public TraktUriService(TraktApi traktApi, ILogger logger, ILibraryManager libraryManager)
{
_traktApi = traktApi;
_logger = logger;
_libraryManager = libraryManager;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
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;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
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;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public object Post(RecommendedMovies request)
{
return _traktApi.SendMovieRecommendationsRequest(UserHelper.GetTraktUser(request.UserId)).Result;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public object Post(RecommendedShows request)
{
return _traktApi.SendShowRecommendationsRequest(UserHelper.GetTraktUser(request.UserId)).Result;
}
}
}

1021
Trakt/Api/TraktApi.cs Normal file

File diff suppressed because it is too large Load Diff

43
Trakt/Api/TraktURIs.cs Normal file
View File

@ -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
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,35 @@
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins;
using System.IO;
namespace Trakt.Configuration
{
/// <summary>
/// Class TraktConfigurationPage
/// </summary>
class TraktConfigurationPage : IPluginConfigurationPage
{
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
public string Name => "Trakt for MediaBrowser";
/// <summary>
/// Gets the HTML stream.
/// </summary>
/// <returns>Stream.</returns>
public Stream GetHtmlStream()
{
return GetType().Assembly.GetManifestResourceStream(GetType().Namespace + ".configPage.html");
}
/// <summary>
/// Gets the type of the configuration page.
/// </summary>
/// <value>The type of the configuration page.</value>
public ConfigurationPageType ConfigurationPageType => ConfigurationPageType.PluginConfiguration;
public IPlugin Plugin => Trakt.Plugin.Instance;
}
}

View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html>
<head>
<title>Trakt</title>
</head>
<body>
<!-- ReSharper disable UnknownCssClass -->
<div id="traktConfigurationPage" data-role="page" class="page type-interior pluginConfigurationPage">
<div data-role="content">
<div class="content-primary">
<form id="traktConfigurationForm">
<ul class="ulForm" data-role="listview">
<li>
<label for="selectUser">Configure Trakt for:</label>
<select id="selectUser" name="selectUser" onchange=" TraktConfigurationPage.loadConfiguration(this.value); "></select>
</li>
<li>
<label for="txtTraktUserName">
Trakt Username:
</label>
<input id="txtTraktUserName" name="txtTraktUserName" type="text" />
</li>
<li>
<label for="txtTraktPassword">
Password:
</label>
<input id="txtTraktPassword" name="txtTraktPassword" type="password" />
</li>
<li>
<div data-mini="true" data-role="collapsible">
<h3>Locations Excluded:</h3>
<div id="divTraktLocations">
</div>
</div>
</li>
<li>
<input type="checkbox" id="chkSkipUnwatchedImportFromTrakt" name="chkSkipUnwatchedImportFromTrakt" />
<label for="chkSkipUnwatchedImportFromTrakt">Skip unwatched import from trakt</label>
<div class="fieldDescription">
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.
</div>
</li>
<li>
<input type="checkbox" id="chkPostWatchedHistory" name="chkPostWatchedHistory" />
<label for="chkPostWatchedHistory">Change watched history on trakt</label>
<div class="fieldDescription">
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).
</div>
</li>
<li>
<input type="checkbox" id="chkExtraLogging" name="chkExtraLogging" />
<label for="chkExtraLogging">Add Extra Logs</label>
<div class="fieldDescription">
When enabled, all data sent to trakt is logged.
</div>
</li>
<li>
<input type="checkbox" id="chkExportMediaInfo" name="chkExportMediaInfo" />
<label for="chkExportMediaInfo">Export MediaInfo Metadata</label>
<div class="fieldDescription">
Send audio and video metadata to Trakt.
</div>
</li>
<li>
<button type="submit" data-theme="b">Save</button>
<button type="button" onclick=" history.back(); ">Cancel</button>
</li>
</ul>
</form>
</div>
</div>
<!-- ReSharper disable UseOfImplicitGlobalInFunctionScope -->
<script type="text/javascript">
Array.prototype.remove = function () {
var what, a = arguments, L = a.length, ax;
while (L && this.length) {
what = a[--L];
while ((ax = this.indexOf(what)) !== -1) {
this.splice(ax, 1);
}
}
return this;
};
var TraktConfigurationPage =
{
pluginUniqueId: "8abc6789-fde2-4705-8592-4028806fa343",
loadConfiguration: function (userId) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TraktConfigurationPage.pluginUniqueId).then(function (config) {
var currentUserConfig = config.TraktUsers.filter(function (curr) {
return curr.LinkedMbUserId == userId;
//return true;
})[0];
var page = $.mobile.activePage;
// User doesn't have a config, so create a default one.
if (!currentUserConfig) {
// You don't have to put every property in here, just the ones the UI is expecting (below)
currentUserConfig = {
UserName: "",
Password: "",
SkipUnwatchedImportFromTrakt: false,
PostWatchedHistory: true,
ExtraLogging: false,
ExportMediaInfo: false
};
}
// Default this to an empty array so the rendering code doesn't have to worry about it
currentUserConfig.LocationsExcluded = currentUserConfig.LocationsExcluded || [];
$('#txtTraktUserName', page).val(currentUserConfig.UserName);
$('#txtTraktPassword', page).val(currentUserConfig.Password);
$('#chkSkipUnwatchedImportFromTrakt', page).checked(currentUserConfig.SkipUnwatchedImportFromTrakt).checkboxradio("refresh");
$('#chkPostWatchedHistory', page).checked(currentUserConfig.PostWatchedHistory).checkboxradio("refresh");
$('#chkExtraLogging', page).checked(currentUserConfig.ExtraLogging).checkboxradio("refresh");
$('#chkExportMediaInfo', page).checked(currentUserConfig.ExportMediaInfo).checkboxradio("refresh");
// List the folders the user can access
ApiClient.getVirtualFolders(userId).then(function (result) {
TraktConfigurationPage.loadFolders(currentUserConfig, result);
});
Dashboard.hideLoadingMsg();
});
},
populateUsers: function (users) {
var html = "";
for (var i = 0, length = users.length; i < length; i++) {
var user = users[i];
html += '<option value="' + user.Id + '">' + user.Name + '</option>';
}
$('#selectUser', $.mobile.activePage).html(html).selectmenu("refresh");
},
loadFolders: function (currentUserConfig, virtualFolders) {
var page = $.mobile.activePage;
var html = "";
html += '<div data-role="controlgroup">';
for (var i = 0, length = virtualFolders.length; i < length; i++) {
var virtualFolder = virtualFolders[i];
html += TraktConfigurationPage.getFolderHtml(currentUserConfig, virtualFolder, i);
}
html += '</div>';
$('#divTraktLocations', page).html(html).trigger('create');
},
getFolderHtml: function (currentUserConfig, virtualFolder, index) {
var html = "";
for (var i = 0, length = virtualFolder.Locations.length; i < length; i++) {
var id = "chkFolder" + index + "_" + i;
var location = virtualFolder.Locations[i];
var isChecked = currentUserConfig.LocationsExcluded.filter(function (current) {
return current.toLowerCase() == location.toLowerCase();
}).length;
var checkedAttribute = isChecked ? 'checked="checked"' : "";
html += '<label for="' + id + '">' + location + '</label>';
html += '<input class="chkTraktLocation" type="checkbox" data-mini="true" id="' + id + '" name="' + id + '" data-location="' + location + '" ' + checkedAttribute + ' />';
}
return html;
}
};
$('#traktConfigurationPage').on('pageshow', function () {
Dashboard.showLoadingMsg();
var page = $.mobile.activePage;
ApiClient.getUsers().then(function (users) {
TraktConfigurationPage.populateUsers(users);
var currentUserId = $('#selectUser', page).val();
TraktConfigurationPage.loadConfiguration(currentUserId);
});
});
$('#traktConfigurationForm').on('submit', function () {
Dashboard.showLoadingMsg();
var page = $.mobile.activePage;
var currentUserId = $('#selectUser', page).val();
ApiClient.getPluginConfiguration(TraktConfigurationPage.pluginUniqueId).then(function (config) {
var currentUserConfig = config.TraktUsers.filter(function (curr) {
return curr.LinkedMbUserId == currentUserId;
})[0];
// User doesn't have a config, so create a default one.
if (!currentUserConfig) {
currentUserConfig = {};
config.TraktUsers.push(currentUserConfig);
}
currentUserConfig.SkipUnwatchedImportFromTrakt = $('#chkSkipUnwatchedImportFromTrakt', page).checked();
currentUserConfig.PostWatchedHistory = $('#chkPostWatchedHistory', page).checked();
currentUserConfig.ExtraLogging = $('#chkExtraLogging', page).checked();
currentUserConfig.ExportMediaInfo = $('#chkExportMediaInfo', page).checked();
currentUserConfig.UserName = $('#txtTraktUserName', page).val();
if (currentUserConfig.Password != $('#txtTraktPassword', page).val()) {
currentUserConfig.Password = $('#txtTraktPassword', page).val();
}
currentUserConfig.LinkedMbUserId = currentUserId;
currentUserConfig.LocationsExcluded = $('.chkTraktLocation:checked', page).map(function () {
return this.getAttribute('data-location');
}).get();
if (currentUserConfig.UserName == '') {
config.TraktUsers.remove(config.TraktUsers.indexOf(currentUserConfig));
}
ApiClient.updatePluginConfiguration(TraktConfigurationPage.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
ApiClient.getUsers().then(function (users) {
TraktConfigurationPage.populateUsers(users);
Dashboard.alert('Settings saved.');
});
});
});
return false;
});
</script>
<!-- ReSharper restore UseOfImplicitGlobalInFunctionScope -->
</div>
<!-- ReSharper restore UnknownCssClass -->
</body>
</html>

9
Trakt/Enums.cs Normal file
View File

@ -0,0 +1,9 @@
namespace Trakt
{
public enum MediaStatus
{
Watching,
Paused,
Stop
}
}

208
Trakt/Extensions.cs Normal file
View File

@ -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<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
var itemsReturned = 0;
var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
var count = list.Count;
var chunks = new List<IEnumerable<T>>();
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
}
}
}

View File

@ -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<LibraryEvent> _queuedEvents;
private Timer _queueTimer;
private readonly ILogger _logger ;
private readonly TraktApi _traktApi;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public LibraryManagerEventsHelper(ILogger logger, TraktApi traktApi)
{
_queuedEvents = new List<LibraryEvent>();
_logger = logger;
_traktApi = traktApi;
}
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="eventType"></param>
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);
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<LibraryEvent> 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);
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedMovieEvents(IEnumerable<LibraryEvent> 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);
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedEpisodeEvents(IEnumerable<LibraryEvent> 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<Episode>();
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
}

View File

@ -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
{
/// <summary>
/// Helper class used to update the watched status of movies/episodes. Attempts to organise
/// requests to lower trakt.tv api calls.
/// </summary>
internal class UserDataManagerEventsHelper
{
private List<UserDataPackage> _userDataPackages;
private readonly ILogger _logger;
private readonly TraktApi _traktApi;
private Timer _timer;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public UserDataManagerEventsHelper(ILogger logger, TraktApi traktApi)
{
_userDataPackages = new List<UserDataPackage>();
_logger = logger;
_traktApi = traktApi;
}
/// <summary>
///
/// </summary>
/// <param name="userDataSaveEventArgs"></param>
/// <param name="traktUser"></param>
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<Movie>();
}
}
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<Movie>();
}
}
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<Episode>();
}
if (userPackage.UnSeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(userPackage.UnSeenEpisodes, userPackage.TraktUser, false,
CancellationToken.None).ConfigureAwait(false);
userPackage.SeenEpisodes = new List<Episode>();
}
userPackage.CurrentSeriesId = episode.Series.Id;
}
if (userDataSaveEventArgs.UserData.Played)
{
userPackage.SeenEpisodes.Add(episode);
}
else
{
userPackage.UnSeenEpisodes.Add(episode);
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
}
}
/// <summary>
/// Class that contains all the items to be reported to trakt.tv and supporting properties.
/// </summary>
internal class UserDataPackage
{
public TraktUser TraktUser;
public Guid CurrentSeriesId;
public List<Movie> SeenMovies;
public List<Movie> UnSeenMovies;
public List<Episode> SeenEpisodes;
public List<Episode> UnSeenEpisodes;
public UserDataPackage()
{
SeenMovies = new List<Movie>();
UnSeenMovies = new List<Movie>();
SeenEpisodes = new List<Episode>();
UnSeenEpisodes = new List<Episode>();
}
}
}

View File

@ -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;
}
}
}

27
Trakt/Model/TraktUser.cs Normal file
View File

@ -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; }
}
}

29
Trakt/Plugin.cs Normal file
View File

@ -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<PluginConfiguration>
{
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;
}
}

View File

@ -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
//

View File

@ -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;
/// <summary>
/// Task that will Sync each users trakt.tv profile with their local library. This task will only include
/// watched states.
/// </summary>
class SyncFromTraktTask : IScheduledTask
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
private readonly TraktApi _traktApi;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="jsonSerializer"></param>
/// <param name="userManager"></param>
/// <param name="userDataManager"> </param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
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);
}
/// <summary>
///
/// </summary>
/// <param name="cancellationToken"></param>
/// <param name="progress"></param>
/// <returns></returns>
public async Task Execute(CancellationToken cancellationToken, IProgress<double> 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<double> progress, double percentPerUser)
{
var libraryRoot = user.RootFolder;
var traktUser = UserHelper.GetTraktUser(user);
IEnumerable<TraktMovieWatched> traktWatchedMovies;
IEnumerable<TraktShowWatched> 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<Movie>())
{
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<Episode>())
{
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<TraktShowWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
public static TraktShowCollected FindMatch(Series item, IEnumerable<TraktShowCollected> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable<TraktMovieWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Movie));
}
public static IEnumerable<TraktMovieCollected> FindMatches(BaseItem item, IEnumerable<TraktMovieCollected> 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;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public IEnumerable<ITaskTrigger> GetDefaultTriggers()
{
return new List<ITaskTrigger>();
}
/// <summary>
///
/// </summary>
public string Name => "Import playstates from Trakt.tv";
/// <summary>
///
/// </summary>
public string Description => "Sync Watched/Unwatched status from Trakt.tv for each MB3 user that has a configured Trakt account";
/// <summary>
///
/// </summary>
public string Category => "Trakt";
}
}

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
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<ITaskTrigger> GetDefaultTriggers()
{
return new List<ITaskTrigger>();
}
public async Task Execute(CancellationToken cancellationToken, IProgress<double> 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<double> 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);
}
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
/// <param name="user">
/// <see cref="User"/> to get <see cref="UserItemData"/> (e.g. watched status) from.
/// </param>
/// <param name="traktUser">
/// The <see cref="TraktUser"/> to sync with.
/// </param>
/// <param name="progress">
/// Progress reporter.
/// </param>
/// <param name="progPercent">
/// Initial progress value.
/// </param>
/// <param name="percentPerItem">
/// Progress percent per item.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token.
/// </param>
/// <returns>
/// Awaitable <see cref="Task"/>.
/// </returns>
private async Task SyncMovies(
User user,
TraktUser traktUser,
IProgress<double> 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<Movie>();
var playedMovies = new List<Movie>();
var unplayedMovies = new List<Movie>();
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<Movie> movies,
IProgress<double> 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<Movie> playedMovies,
IProgress<double> 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<double> 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<Episode>();
var playedEpisodes = new List<Episode>();
var unplayedEpisodes = new List<Episode>();
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<Episode> playedEpisodes,
IProgress<double> 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<Episode> collectedEpisodes,
IProgress<double> 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));
}
}
}
}

310
Trakt/ServerMediator.cs Normal file
View File

@ -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
{
/// <summary>
/// All communication between the server and the plugins server instance should occur in this class.
/// </summary>
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; }
/// <summary>
///
/// </summary>
/// <param name="jsonSerializer"></param>
/// <param name="sessionManager"> </param>
/// <param name="userDataManager"></param>
/// <param name="libraryManager"> </param>
/// <param name="logger"></param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
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;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
/// <summary>
///
/// </summary>
public void Run()
{
_sessionManager.PlaybackStart += KernelPlaybackStart;
_sessionManager.PlaybackStopped += KernelPlaybackStopped;
_libraryManager.ItemAdded += LibraryManagerItemAdded;
_libraryManager.ItemRemoved += LibraryManagerItemRemoved;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
/// <summary>
/// Let Trakt.tv know the user has started to watch something
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
/// <summary>
/// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has
/// completed watching the item.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
/// <summary>
///
/// </summary>
public void Dispose()
{
_sessionManager.PlaybackStart -= KernelPlaybackStart;
_sessionManager.PlaybackStopped -= KernelPlaybackStopped;
_libraryManager.ItemAdded -= LibraryManagerItemAdded;
_libraryManager.ItemRemoved -= LibraryManagerItemRemoved;
_service = null;
_traktApi = null;
_libraryManagerEventsHelper = null;
}
}
/// <summary>
///
/// </summary>
public class ProgressEvent
{
public Guid UserId;
public Guid ItemId;
public DateTime LastApiAccess;
}
}

157
Trakt/Trakt.csproj Normal file
View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7FFC306B-2680-49C7-8BE0-6358B2A8A409}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Trakt</RootNamespace>
<AssemblyName>Trakt</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Interfaces.IO">
<HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="MediaBrowser.Common, Version=3.1.6127.24820, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MediaBrowser.Common.3.0.662\lib\net45\MediaBrowser.Common.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="MediaBrowser.Controller, Version=3.1.6127.24818, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MediaBrowser.Server.Core.3.0.662\lib\net45\MediaBrowser.Controller.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="MediaBrowser.Model, Version=3.1.6127.24820, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MediaBrowser.Common.3.0.662\lib\net45\MediaBrowser.Model.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Patterns.Logging">
<HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath>
</Reference>
<Reference Include="ServiceStack.Interfaces, Version=4.0.0.0, Culture=neutral, PublicKeyToken=e06fbc6124f57c43, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\ServiceStack.Interfaces.4.0.35\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link>
</Compile>
<Compile Include="Api\DataContracts\BaseModel\TraktUserSummary.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktEpisode.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktEpisodeId.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktMovieId.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktShow.cs" />
<Compile Include="Api\DataContracts\Comments\TraktComment.cs" />
<Compile Include="Api\DataContracts\Scrobble\TraktScrobbleResponse.cs" />
<Compile Include="Api\DataContracts\Scrobble\TraktScrobbleEpisode.cs" />
<Compile Include="Api\DataContracts\Scrobble\TraktScrobbleMovie.cs" />
<Compile Include="Api\DataContracts\Sync\Collection\TraktEpisodeCollected.cs" />
<Compile Include="Api\DataContracts\Sync\Collection\TraktMovieCollected.cs" />
<Compile Include="Api\DataContracts\Sync\Collection\TraktShowCollected.cs" />
<Compile Include="Api\DataContracts\Sync\Ratings\TraktEpisodeRated.cs" />
<Compile Include="Api\DataContracts\Sync\Ratings\TraktMovieRated.cs" />
<Compile Include="Api\DataContracts\Sync\Ratings\TraktShowRated.cs" />
<Compile Include="Api\DataContracts\Sync\TraktSync.cs" />
<Compile Include="Api\DataContracts\Sync\Watched\TraktSeasonWatched.cs" />
<Compile Include="Api\DataContracts\Sync\Watched\TraktEpisodeWatched.cs" />
<Compile Include="Api\DataContracts\Sync\Watched\TraktMovieWatched.cs" />
<Compile Include="Api\DataContracts\Sync\Watched\TraktShowWatched.cs" />
<Compile Include="Api\DataContracts\TraktUserTokenRequest.cs" />
<Compile Include="Api\DataContracts\Users\Collection\TraktShowCollected.cs" />
<Compile Include="Api\DataContracts\Users\Ratings\TraktEpisodeRated.cs" />
<Compile Include="Api\DataContracts\Users\Collection\TraktMetadata.cs" />
<Compile Include="Api\DataContracts\Users\Ratings\TraktMovieRated.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktRated.cs" />
<Compile Include="Api\DataContracts\Users\Ratings\TraktSeasonRated.cs" />
<Compile Include="Api\DataContracts\Users\Ratings\TraktShowRated.cs" />
<Compile Include="Api\DataContracts\Users\Collection\TraktMovieCollected.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktPerson.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktPersonId.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktSeason.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktSeasonId.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktId.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktMovie.cs" />
<Compile Include="Api\DataContracts\BaseModel\TraktShowId.cs" />
<Compile Include="Api\DataContracts\Sync\TraktSyncResponse.cs" />
<Compile Include="Api\DataContracts\TraktUserToken.cs" />
<Compile Include="Api\DataContracts\Users\Watched\TraktMovieWatched.cs" />
<Compile Include="Api\DataContracts\Users\Watched\TraktShowWatched.cs" />
<Compile Include="Api\ServerApiEndpoints.cs" />
<Compile Include="Api\TraktApi.cs" />
<Compile Include="Api\TraktURIs.cs" />
<Compile Include="Configuration\PluginConfiguration.cs" />
<Compile Include="Configuration\TraktConfigurationPage.cs" />
<Compile Include="Enums.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="Helpers\LibraryManagerEventsHelper.cs" />
<Compile Include="Helpers\UserDataManagerEventsHelper.cs" />
<Compile Include="Helpers\UserHelpers.cs" />
<Compile Include="Model\TraktUser.cs" />
<Compile Include="Plugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ScheduledTasks\SyncFromTraktTask.cs" />
<Compile Include="ScheduledTasks\SyncLibraryTask.cs" />
<Compile Include="ServerMediator.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\..\Emby.dev\ProgramData-Server\Plugins\" /y</PostBuildEvent>
</PropertyGroup>
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

9
Trakt/packages.config Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="CommonIO" version="1.0.0.9" targetFramework="net45" />
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
<package id="MediaBrowser.Common" version="3.0.662" targetFramework="net45" />
<package id="MediaBrowser.Server.Core" version="3.0.662" targetFramework="net45" />
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
<package id="ServiceStack.Interfaces" version="4.0.35" targetFramework="net45" />
</packages>