Skip to content
This repository has been archived by the owner on Jul 5, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2 from Musi13/channel-api
Browse files Browse the repository at this point in the history
Refactor to Channel API
  • Loading branch information
Musi13 authored Oct 2, 2020
2 parents ee99b2e + 4ba4099 commit 895ad9f
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 214 deletions.
102 changes: 102 additions & 0 deletions Jellyfin.Plugin.Streamlink/Channel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Common.Extensions;

using Jellyfin.Plugin.Streamlink;
using Jellyfin.Plugin.Streamlink.Configuration;
using MediaBrowser.Providers.Plugin.Streamlink;

namespace MediaBrowser.Channels.Streamlink
{
public class Channel : IChannel, IHasCacheKey
{
public Channel() {}

public string HomePageUrl => string.Empty;

public virtual string Name => "Streamlink";

// Increment as needed to invalidate all caches
public string DataVersion => "1";

public Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
{
return Task.FromResult(new ChannelItemResult {
Items = (from c in Plugin.Instance.Configuration.Channels select c.CreateChannelItemInfo()).ToList<ChannelItemInfo>()
});
}

public IEnumerable<ImageType> GetSupportedChannelImages()
{
return new List<ImageType>
{
ImageType.Thumb,
ImageType.Backdrop
};
}

public InternalChannelFeatures GetChannelFeatures()
{
return new InternalChannelFeatures
{
ContentTypes = new List<ChannelMediaContentType>
{
ChannelMediaContentType.Clip
},

MediaTypes = new List<ChannelMediaType>
{
ChannelMediaType.Video
},

SupportsContentDownloading = true
};
}

public Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken)
{
switch (type)
{
case ImageType.Thumb:
case ImageType.Backdrop:
{
var path = GetType().Namespace + ".Images." + type.ToString().ToLowerInvariant() + ".png";

return Task.FromResult(new DynamicImageResponse
{
Format = ImageFormat.Png,
HasImage = true,
Stream = typeof(Channel).Assembly.GetManifestResourceStream(path)
});
}
default:
throw new ArgumentException("Unsupported image type: " + type);
}
}

public bool IsEnabledFor(string userId)
{
return true;
}

public ChannelParentalRating ParentalRating
=> ChannelParentalRating.GeneralAudience;

public string GetCacheKey(string userId)
=> Guid.NewGuid().ToString("N");

public string Description => string.Empty;
}
}
100 changes: 99 additions & 1 deletion Jellyfin.Plugin.Streamlink/Configuration/PluginConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
using MediaBrowser.Model.Plugins;
using System.Globalization;
using System.Collections.Generic;
using System.Diagnostics;

using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Channels;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Common.Extensions;

using MediaBrowser.Providers.Plugin.Streamlink;

namespace Jellyfin.Plugin.Streamlink.Configuration
{
Expand All @@ -9,10 +21,96 @@ public PluginConfiguration()
StreamlinkPath = null;
StreamQuality = "best";
ExtraArguments = "";
Channels = new ChannelConfig[] {};
}

public string StreamlinkPath { get; set; }
public string StreamQuality { get; set; }
public string ExtraArguments { get; set; }
public ChannelConfig[] Channels { get; set; }
}

public class ChannelConfig
{
public string Name { get; set; }
public string Url { get; set; }

public string Id { get => Url.GetMD5().ToString("N", CultureInfo.InvariantCulture); }

public virtual bool IsLive()
{
// This function does work, but each stream costs uses a few seconds to check
// so it currently isn't called when loading the dashboard.
var proc = new Process();
proc.StartInfo.FileName = Jellyfin.Plugin.Streamlink.Plugin.Instance.Configuration.StreamlinkPath;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.ArgumentList.Add("--quiet");
proc.StartInfo.ArgumentList.Add("--stream-url");
proc.StartInfo.ArgumentList.Add(Url);
proc.Start();
proc.WaitForExit();
return proc.ExitCode == 0;
}

public virtual ChannelItemInfo CreateChannelItemInfo()
{
return new ChannelItemInfo
{
Name = Name,
Id = Id,
Type = ChannelItemType.Media,
ContentType = ChannelMediaContentType.Clip,
MediaType = ChannelMediaType.Video,
IsLiveStream = true,
MediaSources = new List<MediaSourceInfo>{ CreateMediaSourceInfo() }
};
}

public virtual MediaSourceInfo CreateMediaSourceInfo()
{
var mediaSource = new MediaSourceInfo
{
Path = Url,
Protocol = MediaProtocol.File,
MediaStreams = new List<MediaStream>
{
new MediaStream
{
Type = MediaStreamType.Video,
// Set the index to -1 because we don't know the exact index of the video stream within the container
// For twitch streams it seems to be 1, but that might not be consistent
Index = -1,
IsInterlaced = true
},
new MediaStream
{
Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container
// For twitch streams it seems to be 0
Index = -1
}
},
RequiresOpening = true,
RequiresClosing = true,
RequiresLooping = false,

OpenToken = StreamlinkProvider.Prefix + Id,

ReadAtNativeFramerate = false,

Id = Id,
IsInfiniteStream = true,
IsRemote = true,

IgnoreDts = true,
SupportsDirectPlay = true,
SupportsDirectStream = true
};

mediaSource.InferTotalBitrate();

return mediaSource;
}
}
}
127 changes: 126 additions & 1 deletion Jellyfin.Plugin.Streamlink/Configuration/configPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,88 @@
</button>
</div>
</form>
<div class="readOnlyContent">
<div style="display: flex; align-items: center;margin: 1em 0;">
<h1 style="margin:0;">Streamlink Streams</h1>
<button is="emby-button" type="button" class="raised btnAdd submit mini" style="margin-left:1em;" title="Add" onclick="StreamlinkConfigurationPage.addStreamPopup();">
<span>Add Stream</span>
</button>
</div>
<div class="streamList paperList">
</div>
</div>
</div>
<div data-role="popup" id="streamPopup" class="dialog dialog-fixedSize dialog-medium-tall hide" style="position: fixed; top: 10%;">
<form id="streamForm" style="padding:1em;">
<div class="ui-bar-a" style="text-align: center; padding: 0 20px;">
<h3>Add Stream</h3>
</div>
<div data-role="content">
<div class="inputContainer">
<input is="emby-input" type="text" autocomplete="off" id="name" label="Name" />
</div>
<div class="inputContainer">
<input is="emby-input" type="url" autocomplete="off" id="url" label="Url:" />
</div>
<p>
<button is="emby-button" type="submit" class="raised button-submit block" data-icon="check" data-mini="true">
<span>Add</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block" data-icon="delete" onclick="$('#streamPopup').addClass('hide');" data-mini="true">
<span>${ButtonCancel}</span>
</button>
</p>
</div>
</form>
</div>
</div>

<script type="text/javascript">
var StreamlinkConfigurationPage = {
pluginUniqueId: "87c22ffb-736c-41aa-8a2d-95d46977bb26",

populateStreamList: function (page) {
var streams = StreamlinkConfigurationPage.config.Channels;
var html = "";

for (var i = 0; i < streams.length; i++) {
var stream = streams[i];

html += '<div class="listItem">';
html += '<i class="listItemIcon md-icon"></i>';
html += '<div class="listItemBody two-line">';
html += '<h3 class="listItemBodyText">';
html += stream.Name;
html += '</h3>';
html += '</div>';
html += '<button type="button" is="paper-icon-button-light" onclick="StreamlinkConfigurationPage.deleteStream(this, ' + i + ');"title="Delete"><i class="md-icon">delete</i></button>';
html += '</div>';
}

var streamList = page.querySelector('.streamList');
streamList.innerHTML = html;

if (streams.length) {
streamList.classList.remove('hide');
} else {
streamList.classList.add('hide');
}
},

loadConfiguration: function() {
Dashboard.showLoadingMsg();

ApiClient.getPluginConfiguration(StreamlinkConfigurationPage.pluginUniqueId).then(function (config) {
var page = $.mobile.activePage;
StreamlinkConfigurationPage.config = config;

$('#streamlinkPath', page).val(config.StreamlinkPath).change();
$('#streamQuality', page).val(config.StreamQuality).change();
$('#extraArguments', page).val(config.ExtraArguments).change();

Dashboard.hideLoadingMsg();
StreamlinkConfigurationPage.populateStreamList(page);

Dashboard.hideLoadingMsg(page);
});
},

Expand All @@ -61,12 +125,68 @@
config.StreamlinkPath = $('#streamlinkPath').val();
config.StreamQuality = $('#streamQuality').val();
config.ExtraArguments = $('#extraArguments').val();
config.Channels = StreamlinkConfigurationPage.config.Channels;

ApiClient.updatePluginConfiguration(StreamlinkConfigurationPage.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});

StreamlinkConfigurationPage.populateStreamList(page);
});
},

deleteStream: function (button, index) {
var msg = "Are you sure you wish to delete this channel?";

require(['confirm'], function (confirm) {
confirm(msg, "Delete Channel").then(function () {
StreamlinkConfigurationPage.config.Channels.splice(index, 1);
var page = $(button).parents('.page')[0];

StreamlinkConfigurationPage.saveConfiguration();
StreamlinkConfigurationPage.populateStreamList(page);
});
});
},

addStreamPopup: function () {
var page = $.mobile.activePage;
$('#name', page).val("").focus();
$('#url', page).val("").focus();
$('#streamPopup', page).removeClass('hide');
$('#name', page).focus();
},

addStream: function (page) {
var page = $.mobile.activePage;
$('#streamPopup', page).addClass('hide');
var form = this;

var newEntry = true;
var name = $('#name', page).val();
var url = $('#url', page).val();
var userId = Dashboard.getCurrentUserId();

if (StreamlinkConfigurationPage.config.Channels.length > 0) {
for (var i = 0, length = StreamlinkConfigurationPage.config.Channels.length; i < length; i++) {
if (StreamlinkConfigurationPage.config.Channels[i].Name == name) {
newEntry = false;
StreamlinkConfigurationPage.config.Channels[i].Url = url;
}
}
}

if (newEntry) {
var conf = {};

conf.Name = name;
conf.Url = url;
StreamlinkConfigurationPage.config.Channels.push(conf);
}
StreamlinkConfigurationPage.saveConfiguration();
StreamlinkConfigurationPage.populateStreamList(page);
return false;
}
};

$('#streamlinkConfigurationPage').on('pageshow', function () {
Expand All @@ -77,6 +197,11 @@
StreamlinkConfigurationPage.saveConfiguration();
return false;
});

$("#streamForm").on('submit', function (e) {
StreamlinkConfigurationPage.addStream();
return false;
})
</script>
</div>
</body>
Expand Down
4 changes: 2 additions & 2 deletions Jellyfin.Plugin.Streamlink/Jellyfin.Plugin.Streamlink.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>Jellyfin.Plugin.Streamlink</RootNamespace>
<AssemblyVersion>1.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 895ad9f

Please sign in to comment.