mirror of
https://github.com/jellyfin/jellyfin-plugin-webhook.git
synced 2024-11-27 00:00:28 +00:00
initial commit
This commit is contained in:
commit
9f3a93b25c
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
330
.gitignore
vendored
Normal file
330
.gitignore
vendored
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
16
Jellyfin.Plugin.Webhook.sln
Normal file
16
Jellyfin.Plugin.Webhook.sln
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Webhook", "Jellyfin.Plugin.Webhook\Jellyfin.Plugin.Webhook.csproj", "{4BE7DEEF-3ACC-4023-8066-5EA3B5CA1A7C}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{4BE7DEEF-3ACC-4023-8066-5EA3B5CA1A7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4BE7DEEF-3ACC-4023-8066-5EA3B5CA1A7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4BE7DEEF-3ACC-4023-8066-5EA3B5CA1A7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4BE7DEEF-3ACC-4023-8066-5EA3B5CA1A7C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
18
Jellyfin.Plugin.Webhook/Configuration/Constants.cs
Normal file
18
Jellyfin.Plugin.Webhook/Configuration/Constants.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace Jellyfin.Plugin.Webhook.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constants.
|
||||||
|
/// </summary>
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recheck interval ms.
|
||||||
|
/// </summary>
|
||||||
|
public const int RecheckIntervalMs = 10_000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Max retries.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxRetries = 10;
|
||||||
|
}
|
||||||
|
}
|
37
Jellyfin.Plugin.Webhook/Configuration/PluginConfiguration.cs
Normal file
37
Jellyfin.Plugin.Webhook/Configuration/PluginConfiguration.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Jellyfin.Plugin.Webhook.Destinations.Discord;
|
||||||
|
using Jellyfin.Plugin.Webhook.Destinations.Gotify;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Webhook plugin configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class PluginConfiguration : BasePluginConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public PluginConfiguration()
|
||||||
|
{
|
||||||
|
DiscordOptions = Array.Empty<DiscordOption>();
|
||||||
|
GotifyOptions = Array.Empty<GotifyOption>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the jellyfin server url.
|
||||||
|
/// </summary>
|
||||||
|
public string ServerUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the discord options.
|
||||||
|
/// </summary>
|
||||||
|
public DiscordOption[] DiscordOptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the gotify options.
|
||||||
|
/// </summary>
|
||||||
|
public GotifyOption[] GotifyOptions { get; set; }
|
||||||
|
}
|
||||||
|
}
|
103
Jellyfin.Plugin.Webhook/Configuration/Web/config.html
Normal file
103
Jellyfin.Plugin.Webhook/Configuration/Web/config.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<div data-role="page" id="webhookConfigurationPage" class="page type-interior pluginConfigurationPage fullWidthContent"
|
||||||
|
data-controller="__plugin/WebhookJS">
|
||||||
|
<div class="content-primary">
|
||||||
|
<div class="verticalSection">
|
||||||
|
<div class="sectionTitleContainer">
|
||||||
|
<h2 class="sectionTitle">Webhook</h2>
|
||||||
|
<a is="emby-linkbutton" class="raised raised-mini" style="margin-left: 2em;" target="_blank"
|
||||||
|
href="https://github.com/crobibero/jellyfin-plugin-webhook">
|
||||||
|
<i class="md-icon button-icon button-icon-left secondaryText"></i>
|
||||||
|
<span>Help</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="webhookConfigurationForm">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<input is="emby-input" type="text" id="txtServerUrl" required="required" label="Server Url:"/>
|
||||||
|
<span>For linking to content. Include base url.</span>
|
||||||
|
</div>
|
||||||
|
<button id="btnAddDiscord" is="emby-button" type="button" class="raised button block">
|
||||||
|
<span>Add Discord Destination</span>
|
||||||
|
</button>
|
||||||
|
<br/>
|
||||||
|
<div id="configurationWrapper">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<button id="saveConfig" is="emby-button" type="submit" class="raised button-submit block">
|
||||||
|
<span>Save</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template id="template-base">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<input is="emby-input" type="text" data-name="txtWebhookUri" required="required" label="Webhook Url:"/>
|
||||||
|
<span>For linking to content. Include base url.</span>
|
||||||
|
</div>
|
||||||
|
<div id="notificationTypeGroup" style="padding-left: 35px;">
|
||||||
|
<label class="checkboxContainer">
|
||||||
|
<input is="emby-checkbox" type="checkbox" data-name="chkEnableMovies"/>
|
||||||
|
<span>Movies</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkboxContainer">
|
||||||
|
<input is="emby-checkbox" type="checkbox" data-name="chkEnableEpisodes"/>
|
||||||
|
<span>Episodes</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkboxContainer">
|
||||||
|
<input is="emby-checkbox" type="checkbox" data-name="chkEnableSeasons"/>
|
||||||
|
<span>Season</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkboxContainer">
|
||||||
|
<input is="emby-checkbox" type="checkbox" data-name="chkEnableSeries"/>
|
||||||
|
<span>Series</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkboxContainer">
|
||||||
|
<input is="emby-checkbox" type="checkbox" data-name="chkEnableAlbums"/>
|
||||||
|
<span>Albums</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkboxContainer">
|
||||||
|
<input is="emby-checkbox" type="checkbox" data-name="chkEnableSongs"/>
|
||||||
|
<span>Songs</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label>Template:</label>
|
||||||
|
<div>
|
||||||
|
<textarea data-name="txtTemplate" style="width: 100%; height: 400px"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="template-discord">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<input is="emby-input" type="text" data-name="txtAvatarUrl" label="Avatar Url:"/>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<input is="emby-input" type="text" data-name="txtUsername" label="Webhook Username:"/>
|
||||||
|
</div>
|
||||||
|
<div class="selectContainer">
|
||||||
|
<select is="emby-select" data-name="ddlMentionType" label="Mention Type:">
|
||||||
|
<option value="Everyone">@everyone</option>
|
||||||
|
<option value="Here">@here</option>
|
||||||
|
<option value="None">None</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label>Embed Color</label>
|
||||||
|
<input type="color" data-name="EmbedColor" is="emby-input" style="height: 2.2em;"/>
|
||||||
|
<input type="text" is="emby-input" data-name="txtEmbedColor"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.checkboxContainer {
|
||||||
|
max-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#embedColor:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
235
Jellyfin.Plugin.Webhook/Configuration/Web/config.js
Normal file
235
Jellyfin.Plugin.Webhook/Configuration/Web/config.js
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
define([
|
||||||
|
"loading",
|
||||||
|
"dialogHelper",
|
||||||
|
"emby-input",
|
||||||
|
"emby-button",
|
||||||
|
"emby-collapse",
|
||||||
|
"formDialogStyle",
|
||||||
|
"flexStyles"
|
||||||
|
], function (loading, dialogHelper) {
|
||||||
|
var pluginId = "529397D0-A0AA-43DB-9537-7CFDE936C1E3";
|
||||||
|
var discordDefaultEmbedColor = "#AA5CC3";
|
||||||
|
var defaultDiscordTemplate = `{
|
||||||
|
"content": "{{MentionType}}",
|
||||||
|
"avatar_url": "{{AvatarUrl}}",
|
||||||
|
"username": "{{Username}}",
|
||||||
|
"embeds": [
|
||||||
|
\t{
|
||||||
|
\t\t"color": "{{EmbedColor}}",
|
||||||
|
\t\t"footer": {
|
||||||
|
\t\t\t"text": "From {{ServerName}}",
|
||||||
|
\t\t\t"iconUrl": "{{AvatarUrl}}"
|
||||||
|
\t\t},
|
||||||
|
{{if_equals ItemType 'Season'}}
|
||||||
|
"title": "{{SeriesName}} {{Name}} has been added to {{ServerName}}",
|
||||||
|
{{else}}
|
||||||
|
{{if_equals ItemType 'Episode'}}
|
||||||
|
"title": "{{SeriesName}} S{{SeasonNumber}}E{{EpisodeNumber}} {{Name}} has been added to {{ServerName}}",
|
||||||
|
{{else}}
|
||||||
|
"title": "{{Name}} ({{Year}}) has been added to {{ServerName}}",
|
||||||
|
{{/if_equals}}
|
||||||
|
{{/if_equals}}
|
||||||
|
"url": "{{ServerUrl}}/web/index.html#!/details?id={{ItemId}}&serverId={{ServerId}}",
|
||||||
|
"thumbnail":{
|
||||||
|
"url": "{{ServerUrl}}/Items/{{ItemId}}/Images/Primary"
|
||||||
|
}
|
||||||
|
\t}
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
var configurationWrapper = document.querySelector("#configurationWrapper");
|
||||||
|
var templateBase = document.querySelector("#template-base");
|
||||||
|
|
||||||
|
var btnAddDiscord = document.querySelector("#btnAddDiscord");
|
||||||
|
var templateDiscord = document.querySelector("#template-discord");
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
btnAddDiscord.addEventListener("click", addDiscordConfig);
|
||||||
|
document.querySelector("#saveConfig").addEventListener("click", saveConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the base config template.
|
||||||
|
* @param template {HTMLElement} Inner template.
|
||||||
|
* @param name {string} Config name.
|
||||||
|
* @returns {HTMLElement} Wrapped template.
|
||||||
|
*/
|
||||||
|
function addBaseConfig(template, name){
|
||||||
|
var collapse = document.createElement("div");
|
||||||
|
collapse.setAttribute("is", "emby-collapse");
|
||||||
|
collapse.setAttribute("title", name);
|
||||||
|
collapse.dataset.configWrapper = "1";
|
||||||
|
var collapseContent = document.createElement("div");
|
||||||
|
collapseContent.classList.add("collapseContent");
|
||||||
|
|
||||||
|
// Append template content.
|
||||||
|
collapseContent.appendChild(template);
|
||||||
|
|
||||||
|
// Append removal button.
|
||||||
|
var btnRemove = document.createElement("button");
|
||||||
|
btnRemove.innerText = "Remove";
|
||||||
|
btnRemove.setAttribute("is", "emby-button");
|
||||||
|
btnRemove.classList.add("raised", "button-warning", "block");
|
||||||
|
btnRemove.addEventListener("click", removeConfig);
|
||||||
|
|
||||||
|
collapseContent.appendChild(btnRemove);
|
||||||
|
collapse.appendChild(collapseContent);
|
||||||
|
|
||||||
|
return collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds discord configuration.
|
||||||
|
* @param config {Object}
|
||||||
|
*/
|
||||||
|
function addDiscordConfig(config){
|
||||||
|
var template = document.createElement("div");
|
||||||
|
template.dataset.type = "discord";
|
||||||
|
template.appendChild(templateBase.cloneNode(true).content);
|
||||||
|
template.appendChild(templateDiscord.cloneNode(true).content);
|
||||||
|
|
||||||
|
var txtColor = template.querySelector("[data-name=txtEmbedColor]");
|
||||||
|
var selColor = template.querySelector("[data-name=EmbedColor]");
|
||||||
|
txtColor.addEventListener("input", function(){ selColor.value = this.value; });
|
||||||
|
selColor.addEventListener("change", function(){ txtColor.value = this.value; });
|
||||||
|
|
||||||
|
var base = addBaseConfig(template, "Discord");
|
||||||
|
configurationWrapper.appendChild(base);
|
||||||
|
|
||||||
|
// Load configuration.
|
||||||
|
setDiscordConfig(config, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads config into element.
|
||||||
|
* @param config {Object}
|
||||||
|
* @param element {HTMLElement}
|
||||||
|
*/
|
||||||
|
function setBaseConfig(config, element){
|
||||||
|
element.querySelector("[data-name=chkEnableMovies]").checked = config.EnableMovies || true;
|
||||||
|
element.querySelector("[data-name=chkEnableEpisodes]").checked = config.EnableEpisodes || true;
|
||||||
|
element.querySelector("[data-name=chkEnableSeasons]").checked = config.EnableSeasons || true;
|
||||||
|
element.querySelector("[data-name=chkEnableSeries]").checked = config.EnableSeries || true;
|
||||||
|
element.querySelector("[data-name=chkEnableAlbums]").checked = config.EnableAlbums || true;
|
||||||
|
element.querySelector("[data-name=chkEnableSongs]").checked = config.EnableSongs || true;
|
||||||
|
element.querySelector("[data-name=txtWebhookUri]").value = config.WebhookUri || "";
|
||||||
|
element.querySelector("[data-name=txtTemplate]").value = atob(config.Template || "") || defaultDiscordTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads config into element.
|
||||||
|
* @param config {Object}
|
||||||
|
* @param element {HTMLElement}
|
||||||
|
*/
|
||||||
|
function setDiscordConfig(config, element){
|
||||||
|
setBaseConfig(config, element);
|
||||||
|
|
||||||
|
element.querySelector("[data-name=txtAvatarUrl]").value = config.AvatarUrl || "";
|
||||||
|
element.querySelector("[data-name=txtUsername]").value = config.Username || "";
|
||||||
|
element.querySelector("[data-name=ddlMentionType]").value = config.MentionType || "None";
|
||||||
|
element.querySelector("[data-name=txtEmbedColor]").value = config.EmbedColor || discordDefaultEmbedColor;
|
||||||
|
element.querySelector("[data-name=EmbedColor]").value = config.EmbedColor || discordDefaultEmbedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base config.
|
||||||
|
* @param element {HTMLElement}
|
||||||
|
* @returns {Object} configuration result.
|
||||||
|
*/
|
||||||
|
function getBaseConfig(element){
|
||||||
|
var config = {};
|
||||||
|
|
||||||
|
config.EnableMovies = element.querySelector("[data-name=chkEnableMovies]").checked || false;
|
||||||
|
config.EnableEpisodes = element.querySelector("[data-name=chkEnableEpisodes]").checked || false;
|
||||||
|
config.EnableSeasons = element.querySelector("[data-name=chkEnableSeasons]").checked || false;
|
||||||
|
config.EnableSeries = element.querySelector("[data-name=chkEnableSeries]").checked || false;
|
||||||
|
config.EnableAlbums = element.querySelector("[data-name=chkEnableAlbums]").checked || false;
|
||||||
|
config.EnableSongs = element.querySelector("[data-name=chkEnableSongs]").checked || false;
|
||||||
|
config.WebhookUri = element.querySelector("[data-name=txtWebhookUri]").value || "";
|
||||||
|
config.Template = btoa(element.querySelector("[data-name=txtTemplate]").value || "");
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get discord specific config.
|
||||||
|
* @param e {HTMLElement}
|
||||||
|
* @returns {Object} configuration result.
|
||||||
|
*/
|
||||||
|
function getDiscordConfig(e){
|
||||||
|
var config = getBaseConfig(e);
|
||||||
|
config.AvatarUrl = e.querySelector("[data-name=txtAvatarUrl]").value || "";
|
||||||
|
config.Username = e.querySelector("[data-name=txtUsername]").value || "";
|
||||||
|
config.MentionType = e.querySelector("[data-name=ddlMentionType]").value || "";
|
||||||
|
config.EmbedColor = e.querySelector("[data-name=txtEmbedColor]").value || "";
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes config from dom.
|
||||||
|
* @param e {Event}
|
||||||
|
*/
|
||||||
|
function removeConfig(e){
|
||||||
|
e.preventDefault();
|
||||||
|
console.log(e);
|
||||||
|
findParentBySelector(e.target, '[data-config-wrapper]').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig(e){
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
var config = {};
|
||||||
|
config.ServerUrl = document.querySelector("#txtServerUrl").value;
|
||||||
|
config.DiscordOptions = [];
|
||||||
|
var discordConfigs = document.querySelectorAll("[data-type=discord]");
|
||||||
|
for(var i = 0; i < discordConfigs.length; i++){
|
||||||
|
config.DiscordOptions.push(getDiscordConfig(discordConfigs[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(config);
|
||||||
|
ApiClient.updatePluginConfiguration(pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig(){
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
|
||||||
|
document.querySelector("#txtServerUrl").value = config.ServerUrl || "";
|
||||||
|
for(var i = 0; i < config.DiscordOptions.length; i++){
|
||||||
|
addDiscordConfig(config.DiscordOptions[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
|
||||||
|
|
||||||
|
/*** Utils ***/
|
||||||
|
function collectionHas(a, b) {
|
||||||
|
for (let i = 0, len = a.length; i < len; i++) {
|
||||||
|
if (a[i] === b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param elm {EventTarget}
|
||||||
|
* @param selector {string}
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
function findParentBySelector(elm, selector) {
|
||||||
|
const all = document.querySelectorAll(selector);
|
||||||
|
let cur = elm.parentNode;
|
||||||
|
//keep going up until you find a match
|
||||||
|
while (cur && !collectionHas(all, cur)) {
|
||||||
|
cur = cur.parentNode;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
});
|
63
Jellyfin.Plugin.Webhook/Destinations/BaseOption.cs
Normal file
63
Jellyfin.Plugin.Webhook/Destinations/BaseOption.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using HandlebarsDotNet;
|
||||||
|
using Jellyfin.Plugin.Webhook.Helpers;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base options for destination.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseOption
|
||||||
|
{
|
||||||
|
private Func<object, string> _compiledTemplate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the webhook uri.
|
||||||
|
/// </summary>
|
||||||
|
public string WebhookUri { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to notify on movies.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableMovies { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to notify on episodes.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableEpisodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to notify on series.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSeries { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to notify on seasons.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSeasons { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to notify on albums.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAlbums { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to notify on songs.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSongs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the handlebars template.
|
||||||
|
/// </summary>
|
||||||
|
public string Template { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the compiled handlebars template.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The compiled handlebars template.</returns>
|
||||||
|
public Func<object, string> GetCompiledTemplate()
|
||||||
|
{
|
||||||
|
return _compiledTemplate ??= Handlebars.Compile(HandlebarsFunctionHelpers.Base64Decode(Template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Model.Net;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations.Discord
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class DiscordDestination : IDestination<DiscordOption>
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscordDestination> _logger;
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DiscordDestination"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{DiscordDestination}"/> interface.</param>
|
||||||
|
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
||||||
|
public DiscordDestination(ILogger<DiscordDestination> logger, IHttpClient httpClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task SendAsync(DiscordOption options, Dictionary<string, object> data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add discord specific properties.
|
||||||
|
data["MentionType"] = GetMentionType(options.MentionType);
|
||||||
|
data["EmbedColor"] = FormatColorCode(options.EmbedColor);
|
||||||
|
data["AvatarUrl"] = options.AvatarUrl;
|
||||||
|
data["Username"] = options.Username;
|
||||||
|
|
||||||
|
var body = options.GetCompiledTemplate()(data);
|
||||||
|
_logger.LogTrace("SendAsync Body: {body}", body);
|
||||||
|
var requestOptions = new HttpRequestOptions
|
||||||
|
{
|
||||||
|
Url = options.WebhookUri,
|
||||||
|
RequestContent = body,
|
||||||
|
RequestContentType = MediaTypeNames.Application.Json
|
||||||
|
};
|
||||||
|
|
||||||
|
using var response = await _httpClient.Post(requestOptions).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (HttpException e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Error sending notification.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FormatColorCode(string hexCode)
|
||||||
|
{
|
||||||
|
return int.Parse(hexCode.Substring(1, 6), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetMentionType(DiscordMentionType mentionType)
|
||||||
|
{
|
||||||
|
return mentionType switch
|
||||||
|
{
|
||||||
|
DiscordMentionType.Everyone => "@everyone",
|
||||||
|
DiscordMentionType.Here => "@here",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations.Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Discord mention type.
|
||||||
|
/// </summary>
|
||||||
|
public enum DiscordMentionType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mention @everyone.
|
||||||
|
/// </summary>
|
||||||
|
Everyone = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mention @here.
|
||||||
|
/// </summary>
|
||||||
|
Here = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mention none.
|
||||||
|
/// </summary>
|
||||||
|
None = 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations.Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Discord specific options.
|
||||||
|
/// </summary>
|
||||||
|
public class DiscordOption : BaseOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the embed color.
|
||||||
|
/// </summary>
|
||||||
|
public string EmbedColor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the avatar url.
|
||||||
|
/// </summary>
|
||||||
|
public string AvatarUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the bot username.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the mention type.
|
||||||
|
/// </summary>
|
||||||
|
public DiscordMentionType MentionType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Plugin.Webhook.Destinations.Discord;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations.Gotify
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gotify destination.
|
||||||
|
/// </summary>
|
||||||
|
public class GotifyDestination : IDestination<GotifyOption>
|
||||||
|
{
|
||||||
|
private readonly ILogger<GotifyDestination> _logger;
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GotifyDestination"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{GotifyDestination}"/> interface.</param>
|
||||||
|
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
||||||
|
public GotifyDestination(ILogger<GotifyDestination> logger, IHttpClient httpClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SendAsync(GotifyOption options, Dictionary<string, object> message)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations.Gotify
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gotify specific options.
|
||||||
|
/// </summary>
|
||||||
|
public class GotifyOption : BaseOption
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
21
Jellyfin.Plugin.Webhook/Destinations/IDestination.cs
Normal file
21
Jellyfin.Plugin.Webhook/Destinations/IDestination.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Destinations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Destination interface.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDestinationOptions">The type of options.</typeparam>
|
||||||
|
public interface IDestination<in TDestinationOptions>
|
||||||
|
where TDestinationOptions : BaseOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Send message to destination.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The destination options.</param>
|
||||||
|
/// <param name="message">The message to send.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task SendAsync(TDestinationOptions options, Dictionary<string, object> message);
|
||||||
|
}
|
||||||
|
}
|
75
Jellyfin.Plugin.Webhook/Helpers/HandlebarsFunctionHelpers.cs
Normal file
75
Jellyfin.Plugin.Webhook/Helpers/HandlebarsFunctionHelpers.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using HandlebarsDotNet;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handlebar helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static class HandlebarsFunctionHelpers
|
||||||
|
{
|
||||||
|
private static readonly HandlebarsBlockHelper StringEqualityHelper = (output, options, context, arguments) =>
|
||||||
|
{
|
||||||
|
if (arguments.Length != 2)
|
||||||
|
{
|
||||||
|
throw new HandlebarsException("{{if_equals}} helper must have exactly two arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = arguments[0] as string;
|
||||||
|
var right = arguments[1] as string;
|
||||||
|
if (string.Equals(left, right, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
options.Template(output, context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
options.Inverse(output, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HandlebarsBlockHelper StringExistHelper = (output, options, context, arguments) =>
|
||||||
|
{
|
||||||
|
if (arguments.Length != 1)
|
||||||
|
{
|
||||||
|
throw new HandlebarsException("{{if_equals}} helper must have exactly one argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
var arg = arguments[0] as string;
|
||||||
|
if (string.IsNullOrEmpty(arg))
|
||||||
|
{
|
||||||
|
options.Inverse(output, context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
options.Template(output, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register handlebars helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static void RegisterHelpers()
|
||||||
|
{
|
||||||
|
Handlebars.RegisterHelper("if_equals", StringEqualityHelper);
|
||||||
|
Handlebars.RegisterHelper("if_exist", StringExistHelper);
|
||||||
|
Handlebars.RegisterHelper("link_to", (writer, context, parameters) =>
|
||||||
|
{
|
||||||
|
writer.WriteSafeString($"<a href='{(object)context.url}'>{(object)context.text}</a>");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base 64 decode.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The template is stored as base64 in config.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="base64EncodedData">The encoded data.</param>
|
||||||
|
/// <returns>The decoded string.</returns>
|
||||||
|
public static string Base64Decode(string base64EncodedData)
|
||||||
|
{
|
||||||
|
var base64EncodedBytes = Convert.FromBase64String(base64EncodedData);
|
||||||
|
return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Jellyfin.Plugin.Webhook/Helpers/PeriodicAsyncHelper.cs
Normal file
32
Jellyfin.Plugin.Webhook/Helpers/PeriodicAsyncHelper.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Periodic async helper.
|
||||||
|
/// </summary>
|
||||||
|
public static class PeriodicAsyncHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs an async function periodically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskFactory">The task factory.</param>
|
||||||
|
/// <param name="interval">The run interval.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
public static async Task PeriodicAsync(
|
||||||
|
Func<Task> taskFactory,
|
||||||
|
TimeSpan interval,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var delayTask = Task.Delay(interval, cancellationToken);
|
||||||
|
await taskFactory().ConfigureAwait(false);
|
||||||
|
await delayTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
Jellyfin.Plugin.Webhook/Jellyfin.Plugin.Webhook.csproj
Normal file
35
Jellyfin.Plugin.Webhook/Jellyfin.Plugin.Webhook.csproj
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<NoWarn>CA1707;CA1819</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Handlebars.Net" Version="1.10.1" />
|
||||||
|
<PackageReference Include="Jellyfin.Controller" Version="10.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Code Analyzers-->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Configuration\Web\config.html" />
|
||||||
|
<EmbeddedResource Include="Configuration\Web\config.html" />
|
||||||
|
<None Remove="Configuration\Web\config.js" />
|
||||||
|
<EmbeddedResource Include="Configuration\Web\config.js" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
30
Jellyfin.Plugin.Webhook/Models/QueuedItemContainer.cs
Normal file
30
Jellyfin.Plugin.Webhook/Models/QueuedItemContainer.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Queued item container.
|
||||||
|
/// </summary>
|
||||||
|
public class QueuedItemContainer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="QueuedItemContainer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The item id.</param>
|
||||||
|
public QueuedItemContainer(Guid id)
|
||||||
|
{
|
||||||
|
ItemId = id;
|
||||||
|
RetryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current retry count.
|
||||||
|
/// </summary>
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current item id.
|
||||||
|
/// </summary>
|
||||||
|
public Guid ItemId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
324
Jellyfin.Plugin.Webhook/Notifiers/LibraryAddedNotifier.cs
Normal file
324
Jellyfin.Plugin.Webhook/Notifiers/LibraryAddedNotifier.cs
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Plugin.Webhook.Destinations;
|
||||||
|
using Jellyfin.Plugin.Webhook.Destinations.Discord;
|
||||||
|
using Jellyfin.Plugin.Webhook.Destinations.Gotify;
|
||||||
|
using Jellyfin.Plugin.Webhook.Helpers;
|
||||||
|
using Jellyfin.Plugin.Webhook.Models;
|
||||||
|
using MediaBrowser.Common;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Notifications;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Constants = Jellyfin.Plugin.Webhook.Configuration.Constants;
|
||||||
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
|
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||||
|
using Season = MediaBrowser.Controller.Entities.TV.Season;
|
||||||
|
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook.Notifiers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Notifier when a library item is added.
|
||||||
|
/// </summary>
|
||||||
|
public class LibraryAddedNotifier : INotificationService, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<LibraryAddedNotifier> _logger;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IApplicationHost _applicationHost;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<Guid, QueuedItemContainer> _itemProcessQueue;
|
||||||
|
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||||
|
|
||||||
|
private readonly DiscordDestination _discordDestination;
|
||||||
|
private readonly GotifyDestination _gotifyDestination;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LibraryAddedNotifier"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
||||||
|
/// <param name="applicationHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
|
||||||
|
public LibraryAddedNotifier(ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpClient httpClient, IApplicationHost applicationHost)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<LibraryAddedNotifier>();
|
||||||
|
_libraryManager = libraryManager ?? throw new ArgumentNullException(nameof(libraryManager));
|
||||||
|
_applicationHost = applicationHost;
|
||||||
|
|
||||||
|
_itemProcessQueue = new ConcurrentDictionary<Guid, QueuedItemContainer>();
|
||||||
|
_libraryManager.ItemAdded += ItemAddedHandler;
|
||||||
|
|
||||||
|
HandlebarsFunctionHelpers.RegisterHelpers();
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
PeriodicAsyncHelper.PeriodicAsync(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessItemsAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error");
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(Constants.RecheckIntervalMs),
|
||||||
|
_cancellationTokenSource.Token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_discordDestination = new DiscordDestination(
|
||||||
|
loggerFactory.CreateLogger<DiscordDestination>(),
|
||||||
|
httpClient);
|
||||||
|
|
||||||
|
_gotifyDestination = new GotifyDestination(
|
||||||
|
loggerFactory.CreateLogger<GotifyDestination>(),
|
||||||
|
httpClient);
|
||||||
|
|
||||||
|
/*var c = new PluginConfiguration
|
||||||
|
{
|
||||||
|
ServerUrl = "http://localhost:8096",
|
||||||
|
DiscordOptions = new[]
|
||||||
|
{
|
||||||
|
new DiscordOptions
|
||||||
|
{
|
||||||
|
AvatarUrl = "https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/NSIS/modern-install.png",
|
||||||
|
EmbedColor = "#aa5cc3",
|
||||||
|
EnableAlbums = true,
|
||||||
|
EnableSeasons = true,
|
||||||
|
EnableEpisodes = true,
|
||||||
|
EnableMovies = true,
|
||||||
|
EnableSeries = true,
|
||||||
|
EnableSongs = true,
|
||||||
|
Username = "jellybot",
|
||||||
|
MentionType = DiscordMentionType.Here,
|
||||||
|
Template = "ewogICJjb250ZW50IjogInt7TWVudGlvblR5cGV9fSIsCiAgImF2YXRhcl91cmwiOiAie3tBdmF0YXJVcmx9fSIsCiAgInVzZXJuYW1lIjogInt7VXNlcm5hbWV9fSIKICAiZW1iZWRzIjogWwoJewoJCSJjb2xvciI6ICJ7e0VtYmVkQ29sb3J9fSIsCgkJImZvb3RlciI6IHsKCQkJInRleHQiOiAiRnJvbSB7e1NlcnZlck5hbWV9fSIsCgkJCSJpY29uVXJsIjogInt7QXZhdGFyVXJsfX0iCgkJfSwKCQkidGltZXN0YW1wIjogInt7VXRjVGltZXN0YW1wfX0iLAogICAgICAgIHt7I2lzX2VxdWFscyBJdGVtVHlwZSAnU2Vhc29uJ319CiAgICAgICAgICAgICJ0aXRsZSI6ICJ7e1Nlcmllc05hbWV9fSB7e05hbWV9fSBoYXMgYmVlbiBhZGRlZCB0byB7e1NlcnZlck5hbWV9fSIKICAgICAgICB7e2Vsc2V9fQogICAgICAgIHt7I2lzX2VxdWFscyBJdGVtVHlwZSAnRXBpc29kZSd9fQogICAgICAgICAgICAidGl0bGUiOiAie3tTZXJpZXNOYW1lfX0gU3t7U2Vhc29uTnVtYmVyfX1Fe3tFcGlzb2RlTnVtYmVyfX0ge3tOYW1lfX0gaGFzIGJlZW4gYWRkZWQgdG8ge3tTZXJ2ZXJOYW1lfX0iCiAgICAgICAge3tlbHNlfX0KICAgICAgICAgICAgInRpdGxlIjogInt7TmFtZX19ICh7e1llYXJ9fSkgaGFzIGJlZW4gYWRkZWQgdG8ge3tTZXJ2ZXJOYW1lfX0iICAgICAgICAKICAgICAgICB7ey9pc19lcXVhbHN9fSAgICAgICAKICAgICAgICB7ey9pc19lcXVhbHN9fQogICAgICAgICJ1cmwiOiAie3tTZXJ2ZXJVcmx9fS93ZWIvaW5kZXguaHRtbC8jIS9kZXRhaWxzP2lkPXt7SXRlbUlkfX0mc2VydmVySWQ9e3tTZXJ2ZXJJZH19IiwKICAgICAgICAidGh1bWJuYWlsIjp7CiAgICAgICAgICAgICJ1cmwiOiAie3tTZXJ2ZXJVcmx9fS9JdGVtcy97e0l0ZW1JZH19L0ltYWdlcy9QcmltYXJ5IgogICAgICAgIH0KCX0KICBdCn0=",
|
||||||
|
WebhookUri = "https://discordapp.com/api/webhooks/752725480880210000/xJSIE0Pp8E73ojyqXjwPk5EKD3syDabQWBQbmFvqlN9YVnVWPOxcr24oCrJ43Zd415gq"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
WebhookPlugin.Instance.Configuration.ServerUrl = c.ServerUrl;
|
||||||
|
WebhookPlugin.Instance.Configuration.DiscordOptions = c.DiscordOptions;
|
||||||
|
WebhookPlugin.Instance.SaveConfiguration();*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => WebhookPlugin.Instance.Name;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SendNotification(UserNotification request, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsEnabledForUser(User user) => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">Dispose all assets.</param>
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_libraryManager.ItemAdded -= ItemAddedHandler;
|
||||||
|
_cancellationTokenSource.Cancel();
|
||||||
|
_cancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool NotifyOnItem<T>(T baseOptions, Type itemType)
|
||||||
|
where T : BaseOption
|
||||||
|
{
|
||||||
|
_logger.LogDebug("NotifyOnItem");
|
||||||
|
if (baseOptions.EnableAlbums && itemType == typeof(MusicAlbum))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseOptions.EnableMovies && itemType == typeof(Movie))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseOptions.EnableEpisodes && itemType == typeof(Episode))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseOptions.EnableSeries && itemType == typeof(Series))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseOptions.EnableSeasons && itemType == typeof(Season))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseOptions.EnableSongs && itemType == typeof(Audio))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ItemAddedHandler(object sender, ItemChangeEventArgs itemChangeEventArgs)
|
||||||
|
{
|
||||||
|
// Never notify on virtual items.
|
||||||
|
if (itemChangeEventArgs.Item.IsVirtualItem)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_itemProcessQueue.TryAdd(itemChangeEventArgs.Item.Id, new QueuedItemContainer(itemChangeEventArgs.Item.Id));
|
||||||
|
_logger.LogDebug("Queued {itemName} for notification.", itemChangeEventArgs.Item.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessItemsAsync()
|
||||||
|
{
|
||||||
|
_logger.LogDebug("ProcessItemsAsync");
|
||||||
|
// Attempt to process all items in queue.
|
||||||
|
var currentItems = _itemProcessQueue.ToArray();
|
||||||
|
foreach (var (key, container) in currentItems)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(key);
|
||||||
|
_logger.LogDebug("Item {itemName}", item.Name);
|
||||||
|
|
||||||
|
// Metadata not refreshed yet and under retry limit.
|
||||||
|
if (item.ProviderIds.Keys.Count == 0 && container.RetryCount < Constants.MaxRetries)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Requeue {itemName}, no provider ids.", item.Name);
|
||||||
|
container.RetryCount++;
|
||||||
|
_itemProcessQueue.AddOrUpdate(key, container, (_, __) => container);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Notifying for {itemName}", item.Name);
|
||||||
|
|
||||||
|
// Send notification to each configured destination.
|
||||||
|
var itemData = GetDataObject(item);
|
||||||
|
var itemType = item.GetType();
|
||||||
|
foreach (var option in WebhookPlugin.Instance.Configuration.DiscordOptions)
|
||||||
|
{
|
||||||
|
await SendNotification(_discordDestination, option, itemData, itemType).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var option in WebhookPlugin.Instance.Configuration.GotifyOptions)
|
||||||
|
{
|
||||||
|
await SendNotification(_gotifyDestination, option, itemData, itemType).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove item from queue.
|
||||||
|
_itemProcessQueue.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendNotification<T>(IDestination<T> destination, T option, Dictionary<string, object> itemData, Type itemType)
|
||||||
|
where T : BaseOption
|
||||||
|
{
|
||||||
|
if (NotifyOnItem(option, itemType))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await destination.SendAsync(
|
||||||
|
option,
|
||||||
|
itemData)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Unable to send webhook.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, object> GetDataObject(BaseItem item)
|
||||||
|
{
|
||||||
|
var data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
data["Timestamp"] = DateTime.Now;
|
||||||
|
data["UtcTimestamp"] = DateTime.UtcNow;
|
||||||
|
data["Name"] = item.Name;
|
||||||
|
data["ItemId"] = item.Id;
|
||||||
|
data["ServerId"] = _applicationHost.SystemId;
|
||||||
|
data["ServerUrl"] = WebhookPlugin.Instance.Configuration.ServerUrl;
|
||||||
|
data["ServerName"] = _applicationHost.Name;
|
||||||
|
data["ItemType"] = item.GetType().Name;
|
||||||
|
|
||||||
|
if (!item.ProductionYear.HasValue)
|
||||||
|
{
|
||||||
|
data["Year"] = item.ProductionYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (item)
|
||||||
|
{
|
||||||
|
case Season _:
|
||||||
|
if (!string.IsNullOrEmpty(item.Parent?.Name))
|
||||||
|
{
|
||||||
|
data["SeriesName"] = item.Parent.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Parent?.ProductionYear.HasValue ?? false)
|
||||||
|
{
|
||||||
|
data["Year"] = item.Parent.ProductionYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
data["SeasonNumber"] = item.IndexNumber;
|
||||||
|
data["SeasonNumber00"] = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
data["SeasonNumber000"] = item.IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Episode _:
|
||||||
|
if (!string.IsNullOrEmpty(item.Parent?.Parent?.Name))
|
||||||
|
{
|
||||||
|
data["SeriesName"] = item.Parent.Parent.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Parent?.IndexNumber.HasValue ?? false)
|
||||||
|
{
|
||||||
|
data["SeasonNumber"] = item.Parent.IndexNumber;
|
||||||
|
data["SeasonNumber00"] = item.Parent.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
data["SeasonNumber000"] = item.Parent.IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
data["EpisodeNumber"] = item.IndexNumber;
|
||||||
|
data["EpisodeNumber00"] = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
data["EpisodeNumber000"] = item.IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Parent?.Parent?.ProductionYear.HasValue ?? false)
|
||||||
|
{
|
||||||
|
data["Year"] = item.Parent.Parent.ProductionYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (providerKey, providerValue) in item.ProviderIds)
|
||||||
|
{
|
||||||
|
data[$"Provider_{providerKey.ToLowerInvariant()}"] = providerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
Jellyfin.Plugin.Webhook/Templates/Discord.handlebars
Normal file
53
Jellyfin.Plugin.Webhook/Templates/Discord.handlebars
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"content": "{{MentionType}}",
|
||||||
|
"avatar_url": "{{AvatarUrl}}",
|
||||||
|
"username": "{{Username}}",
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"color": "{{EmbedColor}}",
|
||||||
|
"footer": {
|
||||||
|
"text": "From {{ServerName}}",
|
||||||
|
"iconUrl": "{{AvatarUrl}}"
|
||||||
|
},
|
||||||
|
{{if_equals ItemType 'Season'}}
|
||||||
|
"title": "{{SeriesName}} {{Name}} has been added to {{ServerName}}",
|
||||||
|
{{else}}
|
||||||
|
{{if_equals ItemType 'Episode'}}
|
||||||
|
"title": "{{SeriesName}} S{{SeasonNumber00}}E{{EpisodeNumber00}} {{Name}} has been added to {{ServerName}}",
|
||||||
|
{{else}}
|
||||||
|
"title": "{{Name}} ({{Year}}) has been added to {{ServerName}}",
|
||||||
|
{{/if_equals}}
|
||||||
|
{{/if_equals}}
|
||||||
|
"thumbnail":{
|
||||||
|
"url": "{{ServerUrl}}/Items/{{ItemId}}/Images/Primary"
|
||||||
|
},
|
||||||
|
"description": "External Links:\n
|
||||||
|
{{~if_exist Provider_imdb~}}
|
||||||
|
[IMDb](https://www.imdb.com/title/{{Provider_imdb}}/)\n
|
||||||
|
{{~/if_exist~}}
|
||||||
|
{{~if_exist Provider_tmdb~}}
|
||||||
|
{{~if_equals ItemType 'Movie'~}}
|
||||||
|
[TMDb](https://www.themoviedb.org/movie/{{Provider_tmdb}})\n
|
||||||
|
{{~else~}}
|
||||||
|
[TMDb](https://www.themoviedb.org/tv/{{Provider_tmdb}})\n
|
||||||
|
{{~/if_equals~}}
|
||||||
|
{{~/if_exist~}}
|
||||||
|
{{~if_exist Provider_musicbrainzartist~}}
|
||||||
|
[MusicBrainz](https://musicbrainz.org/artist/{{Provider_musicbrainzartist}})\n
|
||||||
|
{{~/if_exist~}}
|
||||||
|
{{if_exist Provider_audiodbartist}}
|
||||||
|
[AudioDb](https://theaudiodb.com/artist/{{Provider_audiodbartist}})\n
|
||||||
|
{{~/if_exist~}}
|
||||||
|
{{~if_exist Provider_musicbrainztrack~}}
|
||||||
|
[MusicBrainz Track](https://musicbrainz.org/track/{{Provider_musicbrainztrack}})\n
|
||||||
|
{{~/if_exist~}}
|
||||||
|
{{~if_exist Provider_musicbrainzalbum~}}
|
||||||
|
[MusicBrainz Album](https://musicbrainz.org/release/{{Provider_musicbrainzalbum}})\n
|
||||||
|
{{~/if_exist~}}
|
||||||
|
{{if_exist Provider_theaudiodbalbum}}
|
||||||
|
[TADb Album](https://theaudiodb.com/album/{{Provider_theaudiodbalbum}})\n
|
||||||
|
{{~/if_exist~}}
|
||||||
|
[Jellyfin]({{ServerUrl}}/web/index.html#!/details?id={{ItemId}}&serverId={{ServerId}})"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
59
Jellyfin.Plugin.Webhook/WebhookPlugin.cs
Normal file
59
Jellyfin.Plugin.Webhook/WebhookPlugin.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Jellyfin.Plugin.Webhook.Configuration;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Webhook
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin entrypoint.
|
||||||
|
/// </summary>
|
||||||
|
public class WebhookPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
|
{
|
||||||
|
private readonly Guid _id = new Guid("529397D0-A0AA-43DB-9537-7CFDE936C1E3");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="WebhookPlugin"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||||
|
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||||
|
public WebhookPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
|
: base(applicationPaths, xmlSerializer)
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets current plugin instance.
|
||||||
|
/// </summary>
|
||||||
|
public static WebhookPlugin Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Guid Id => _id;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Name => "Webhook";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Description => "Sends notifications to various services via webhooks.";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<PluginPageInfo> GetPages()
|
||||||
|
{
|
||||||
|
yield return new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
EmbeddedResourcePath = GetType().Namespace + ".Configuration.Web.config.html"
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = $"{Name}JS",
|
||||||
|
EmbeddedResourcePath = GetType().Namespace + ".Configuration.Web.config.js"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<h1 align="center">Jellyfin Webhook Plugin</h1>
|
||||||
|
<h3 align="center">Part of the <a href="https://jellyfin.org/">Jellyfin Project</a></h3>
|
||||||
|
|
||||||
|
###
|
||||||
|
Repository Url:
|
||||||
|
https://repo.codyrobibero.dev/manifest.json
|
||||||
|
|
||||||
|
Use Handlebars templating engine to format notifications however you wish.
|
||||||
|
|
||||||
|
See `Templates` for sample templates.
|
||||||
|
|
||||||
|
Destinations:
|
||||||
|
- Discord
|
||||||
|
|
||||||
|
TODO
|
||||||
|
- Gotify
|
||||||
|
- Pushbullet
|
||||||
|
- Pushover
|
||||||
|
- Prowl
|
||||||
|
- Teli?
|
15
build.yaml
Normal file
15
build.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: "Webhook"
|
||||||
|
guid: "71552A5A-5C5C-4350-A2AE-EBE451A30173"
|
||||||
|
version: "1.0.0.0"
|
||||||
|
targetAbi: "10.6.0.0"
|
||||||
|
owner: "crobibero"
|
||||||
|
overview: "Sends notifications."
|
||||||
|
description: >
|
||||||
|
Sends notifications to destinations via webhooks.
|
||||||
|
category: "Notifications"
|
||||||
|
artifacts:
|
||||||
|
- "Jellyfin.Plugin.Webhook.dll"
|
||||||
|
- "Handlebars.dll"
|
||||||
|
changelog: >
|
||||||
|
Initial release
|
68
jellyfin.ruleset
Normal file
68
jellyfin.ruleset
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
||||||
|
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
|
||||||
|
<Rule Id="SA1202" Action="Info" />
|
||||||
|
<!-- disable warning SA1204: Static members must appear before non-static members -->
|
||||||
|
<Rule Id="SA1204" Action="Info" />
|
||||||
|
<!-- disable warning SA1404: Code analysis suppression should have justification -->
|
||||||
|
<Rule Id="SA1404" Action="Info" />
|
||||||
|
|
||||||
|
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
|
||||||
|
<Rule Id="SA1009" Action="None" />
|
||||||
|
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
|
||||||
|
<Rule Id="SA1101" Action="None" />
|
||||||
|
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
|
||||||
|
<Rule Id="SA1108" Action="None" />
|
||||||
|
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
|
||||||
|
<Rule Id="SA1128" Action="None" />
|
||||||
|
<!-- disable warning SA1130: Use lambda syntax -->
|
||||||
|
<Rule Id="SA1130" Action="None" />
|
||||||
|
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
|
||||||
|
<Rule Id="SA1200" Action="None" />
|
||||||
|
<!-- disable warning SA1309: Fields must not begin with an underscore -->
|
||||||
|
<Rule Id="SA1309" Action="None" />
|
||||||
|
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
|
||||||
|
<Rule Id="SA1413" Action="None" />
|
||||||
|
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
|
||||||
|
<Rule Id="SA1512" Action="None" />
|
||||||
|
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
|
||||||
|
<Rule Id="SA1515" Action="None" />
|
||||||
|
<!-- disable warning SA1600: Elements should be documented -->
|
||||||
|
<Rule Id="SA1600" Action="None" />
|
||||||
|
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
|
||||||
|
<Rule Id="SA1633" Action="None" />
|
||||||
|
</Rules>
|
||||||
|
|
||||||
|
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
|
||||||
|
<!-- disable warning CA1031: Do not catch general exception types -->
|
||||||
|
<Rule Id="CA1031" Action="Info" />
|
||||||
|
<!-- disable warning CA1032: Implement standard exception constructors -->
|
||||||
|
<Rule Id="CA1032" Action="Info" />
|
||||||
|
<!-- disable warning CA1062: Validate arguments of public methods -->
|
||||||
|
<Rule Id="CA1062" Action="Info" />
|
||||||
|
<!-- disable warning CA1716: Identifiers should not match keywords -->
|
||||||
|
<Rule Id="CA1716" Action="Info" />
|
||||||
|
<!-- disable warning CA1720: Identifiers should not contain type names -->
|
||||||
|
<Rule Id="CA1720" Action="Info" />
|
||||||
|
<!-- disable warning CA1812: internal class that is apparently never instantiated.
|
||||||
|
If so, remove the code from the assembly.
|
||||||
|
If this class is intended to contain only static members, make it static -->
|
||||||
|
<Rule Id="CA1812" Action="Info" />
|
||||||
|
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
|
||||||
|
<Rule Id="CA1822" Action="Info" />
|
||||||
|
<!-- disable warning CA2000: Dispose objects before losing scope -->
|
||||||
|
<Rule Id="CA2000" Action="Info" />
|
||||||
|
|
||||||
|
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
|
||||||
|
<Rule Id="CA1054" Action="None" />
|
||||||
|
<!-- disable warning CA1055: URI return values should not be strings -->
|
||||||
|
<Rule Id="CA1055" Action="None" />
|
||||||
|
<!-- disable warning CA1056: URI properties should not be strings -->
|
||||||
|
<Rule Id="CA1056" Action="None" />
|
||||||
|
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
|
||||||
|
<Rule Id="CA1303" Action="None" />
|
||||||
|
<!-- disable warning CA1308: Normalize strings to uppercase -->
|
||||||
|
<Rule Id="CA1308" Action="None" />
|
||||||
|
</Rules>
|
||||||
|
</RuleSet>
|
Loading…
Reference in New Issue
Block a user