mirror of
https://github.com/jellyfin/jellyfin-plugin-webhook.git
synced 2024-11-23 05:59:58 +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