6 Commits

10 changed files with 4892 additions and 249 deletions
+2 -2
View File
@@ -8,8 +8,8 @@ steps:
repo: phaefelfinger/tv7playlist
tags:
- latest
- '3.0'
- '3.0.0'
- '3.1'
- '3.1.0'
username:
from_secret: docker_username
password:
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Tv7Playlist.Data;
namespace Tv7Playlist.Controllers
{
[Route("api/channels")]
[ApiController]
public class ChannelApiController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly PlaylistContext _playlistContext;
public ChannelApiController(ILogger<HomeController> logger, PlaylistContext playlistContext)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_playlistContext = playlistContext ?? throw new ArgumentNullException(nameof(playlistContext));
}
[HttpGet]
[Route("")]
public async Task<IActionResult> GetAll()
{
var playlistEntries =
await _playlistContext.PlaylistEntries.AsNoTracking().OrderBy(e => e.Position).ToListAsync();
var result = new {Data = playlistEntries};
return Ok(result);
}
[HttpPut]
[Route("disable")]
public async Task<IActionResult> DisableChannels([FromBody] ICollection<Guid> ids)
{
if (ids == null) return BadRequest();
await UpdateEnabledForItems(ids, false);
return Ok();
}
[HttpPut]
[Route("enable")]
public async Task<IActionResult> EnableChannels([FromBody] ICollection<Guid> ids)
{
if (ids == null) return BadRequest();
await UpdateEnabledForItems(ids, true);
return Ok();
}
[HttpDelete]
[Route("")]
public async Task<IActionResult> DeleteChannels([FromBody] ICollection<Guid> ids)
{
if (ids == null) return BadRequest();
foreach (var id in ids)
{
var entry = await _playlistContext.PlaylistEntries.FindAsync(id);
if (entry == null)
{
_logger.LogDebug($"Could not delete! Channel {id} not found");
continue;
}
_logger.LogInformation($"Deleting channel {id} - {entry.Name}");
_playlistContext.PlaylistEntries.Remove(entry);
}
await _playlistContext.SaveChangesAsync();
return Ok();
}
private async Task UpdateEnabledForItems(IEnumerable<Guid> ids, bool isEnabled)
{
foreach (var id in ids)
{
var entry = await _playlistContext.PlaylistEntries.FindAsync(id);
if (entry == null)
{
_logger.LogDebug($"Could not set enabled state! Channel {id} not found");
continue;
}
_logger.LogInformation($"Setting enabled of channel {id} - {entry.Name} to {isEnabled}");
entry.IsEnabled = isEnabled;
entry.Modified = DateTime.Now;
}
await _playlistContext.SaveChangesAsync();
}
}
}
@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Tv7Playlist.Core;
using Tv7Playlist.Data;
namespace Tv7Playlist.Controllers
{
@@ -19,14 +16,13 @@ namespace Tv7Playlist.Controllers
private readonly ILogger<HomeController> _logger;
private readonly IPlaylistBuilder _playlistBuilder;
private readonly PlaylistContext _playlistContext;
public PlaylistApiController(ILogger<HomeController> logger, IPlaylistBuilder playlistBuilder, IAppConfig appConfig, PlaylistContext playlistContext)
public PlaylistApiController(ILogger<HomeController> logger, IPlaylistBuilder playlistBuilder,
IAppConfig appConfig)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_playlistBuilder = playlistBuilder ?? throw new ArgumentNullException(nameof(playlistBuilder));
_appConfig = appConfig ?? throw new ArgumentNullException(nameof(appConfig));
_playlistContext = playlistContext ?? throw new ArgumentNullException(nameof(playlistContext));
}
[HttpGet]
@@ -50,22 +46,6 @@ namespace Tv7Playlist.Controllers
return await GetPlaylistInternal(true);
}
[HttpPut]
[Route("disable")]
public async Task<IActionResult> DisableChannels([FromBody]ICollection<Guid> ids)
{
await UpdateEnabledForItems(ids, false);
return Ok();
}
[HttpPut]
[Route("enable")]
public async Task<IActionResult> EnableChannels([FromBody]ICollection<Guid> ids)
{
await UpdateEnabledForItems(ids, true);
return Ok();
}
private async Task<IActionResult> GetPlaylistInternal(bool useProxy)
{
var playlistStream = await _playlistBuilder.GeneratePlaylistAsync(useProxy);
@@ -87,18 +67,5 @@ namespace Tv7Playlist.Controllers
return downloadFileName;
}
private async Task UpdateEnabledForItems(IEnumerable<Guid> ids, bool isEnabled)
{
foreach (var id in ids)
{
var entry = await _playlistContext.PlaylistEntries.FindAsync(id);
if (entry == null) continue;
entry.IsEnabled = isEnabled;
}
await _playlistContext.SaveChangesAsync();
}
}
}
@@ -29,7 +29,6 @@ namespace Tv7Playlist.Controllers
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, PlaylistEntry updatedEntry)
//[Bind("PlaylistEntry.Id,PlaylistEntry.Position,PlaylistEntry.TrackNumberOverride,PlaylistEntry.NameOverride,PlaylistEntry.IsEnabled")]
{
if (updatedEntry == null) return NotFound();
@@ -54,47 +53,5 @@ namespace Tv7Playlist.Controllers
return View(updatedEntry);
}
[HttpGet]
public async Task<IActionResult> ToggleEnabled(Guid? id)
{
var entry = await _playlistContext.PlaylistEntries.FindAsync(id);
if (entry == null) return NotFound();
entry.IsEnabled = !entry.IsEnabled;
await _playlistContext.SaveChangesAsync();
return RedirectToAction("Index", "Home");
}
[HttpGet]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> Delete(Guid? id)
{
if (id == null) return NotFound();
var entry = await _playlistContext.PlaylistEntries.FindAsync(id);
if (entry == null) return NotFound();
return View(entry);
}
[HttpPost]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid? id)
{
if (id == null) return NotFound();
var entry = await _playlistContext.PlaylistEntries.FindAsync(id);
if (entry == null) return NotFound();
_playlistContext.PlaylistEntries.Remove(entry);
await _playlistContext.SaveChangesAsync();
return RedirectToAction("Index", "Home");
}
}
}
+165 -104
View File
@@ -1,18 +1,9 @@
@using Microsoft.AspNetCore.Mvc.Routing
@model HomeModel;
@model HomeModel;
@{
ViewData["Title"] = "TV7 Playlist";
}
<form method="post">
@* <div class="row"> *@
@* <div class="col col-4"> *@
@* <button class="btn btn-warning" asp-action="DisableSelectedEntries" asp-controller="Home">Disable selected</button> *@
@* <button class="btn btn-info" asp-action="EnableSelectedEntries" asp-controller="Home">Enable selected</button> *@
@* </div> *@
@* </div> *@
<div class="row">
<div class="col col-12">
<table class="table table-hover table-striped" id="playlistTable">
@@ -20,7 +11,6 @@
<tr>
<th></th>
<th></th>
<th>Single Action</th>
<th>Number Import</th>
<th>Number Export</th>
<th>Position</th>
@@ -35,48 +25,106 @@
</tr>
</thead>
<tbody>
@{
for (var i = 0; i < Model.PlaylistEntries.Count; i++)
{
<tr>
<td></td>
<td>@Model.PlaylistEntries[i].Id</td>
<td>
<a class="btn btn-secondary" asp-area="" asp-controller="PlaylistEntry" asp-action="Edit" asp-route-id="@Model.PlaylistEntries[i].Id">Edit</a>
<a class="btn btn-danger" asp-area="" asp-controller="PlaylistEntry" asp-action="Delete" asp-route-id="@Model.PlaylistEntries[i].Id">Delete</a>
@{
if (Model.PlaylistEntries[i].Entry.IsEnabled)
{
<a class="btn btn-warning" asp-area="" asp-controller="PlaylistEntry" asp-action="ToggleEnabled" asp-route-id="@Model.PlaylistEntries[i].Id">Disable</a>
}
else
{
<a class="btn btn-info" asp-area="" asp-controller="PlaylistEntry" asp-action="ToggleEnabled" asp-route-id="@Model.PlaylistEntries[i].Id">Enable</a>
}
}
</td>
<td>@Model.PlaylistEntries[i].Entry.ChannelNumberImport</td>
<td>@Model.PlaylistEntries[i].Entry.ChannelNumberExport</td>
<td>@Model.PlaylistEntries[i].Entry.Position</td>
<td>@Model.PlaylistEntries[i].Entry.Name</td>
<td>@Model.PlaylistEntries[i].Entry.EpgMatchName</td>
<td class="text-center">@Html.Raw(Model.PlaylistEntries[i].Entry.IsEnabled ? "<span class=\"text-primary\">Enabled</span>" : "<span class=\"text-danger\">Disabled</span>")</td>
<td class="text-center">@Html.Raw(Model.PlaylistEntries[i].Entry.IsAvailable ? "<span class=\"text-primary\">yes</span>" : "<span class=\"text-danger\">no</span>")</td>
<td>@Model.PlaylistEntries[i].Entry.UrlProxy</td>
<td>@Model.PlaylistEntries[i].Entry.UrlOriginal</td>
<td>@Model.PlaylistEntries[i].Entry.Created.ToString("g")</td>
<td>@Model.PlaylistEntries[i].Entry.Modified.ToString("g")</td>
</tr>
}
}
</tbody>
<tfoot>
<th></th>
<th></th>
<th>Number Import</th>
<th>Number Export</th>
<th>Position</th>
<th>Name</th>
<th>EPG Name</th>
<th>Enabled</th>
<th>Available</th>
<th>URL Proxy</th>
<th>URL Original</th>
<th>Created</th>
<th>Modified</th>
</tfoot>
</table>
</div>
</div>
<script>
<script>
const urlGet = '@Url.Action("GetAll", "ChannelApi")';
const urlEnable = '@Url.Action("EnableChannels", "ChannelApi")';
const urlDisable = '@Url.Action("DisableChannels", "ChannelApi")';
const urlDelete = '@Url.Action("DeleteChannels", "ChannelApi")';
const urlEdit = '@Url.Action("Edit", "PlaylistEntry")';
$(document).ready(function() {
const table = $('#playlistTable').DataTable({
$('#playlistTable').DataTable({
"ajax": urlGet,
dom: 'Bfrtip',
pageLength: 25,
columns: [
{
data: null,
render: function ( data, type, row ) {return null;}
},
{
data: "id",
name: "eq",
visible: false,
searchable: false
},
{
data: "channelNumberImport",
},
{
data: "channelNumberExport",
},
{
data: "position",
},
{
data: "name",
},
{
data: "epgMatchName",
},
{
data: "isEnabled",
searchable: false,
render: function ( data, type, row, meta ) {
if (data) {
return '<span class="text-primary">Enabled</span>'
} else {
return '<span class="text-danger">Disabled</span>'
}
}
},
{
data: "isAvailable",
searchable: false,
render: function ( data, type, row, meta ) {
if (data) {
return '<span class="text-primary">yes</span>'
} else {
return '<span class="text-danger">no</span>'
}
}
},
{
data: "urlProxy",
},
{
data: "urlOriginal",
},
{
data: "created",
searchable: false,
render: function ( data, type, row, meta ) {
return moment(data).format('YYYY-MM-DD HH:mm');
}
},
{
data: "modified",
searchable: false,
render: function ( data, type, row, meta ) {
return moment(data).format('YYYY-MM-DD HH:mm');
}
}
],
columnDefs: [
{
orderable: false,
@@ -90,11 +138,11 @@ $(document).ready(function() {
},
{
orderable: false,
targets: [2,10,11]
targets: [0,1,9,10]
},
{
searchable: false,
targets: [2,10,11,12,13]
targets: [0,9,10,11,12]
},
],
select: {
@@ -102,78 +150,91 @@ $(document).ready(function() {
selector: 'td:first-child'
},
buttons: [
'pageLength',
'selectAll',
'selectNone',
{
text: 'Select all',
action: function () {
table.rows().select();
}
},
{
text: 'Select none',
action: function () {
table.rows().deselect();
}
},
{
extend: 'selected',
text: 'Disable selected',
enabled: false,
className: 'btn btn-warning',
action: function ( e, dt, node, config ) {
const ids = $.map(table.rows('.selected').data(), function (item) {
return item[1]
const ids = $.map(dt.rows({ selected: true }).data(), function (item) {
return item.id;
});
setEnabledForChannels(urlDisable, ids);
setEnabledForChannels(urlDisable, ids, dt);
},
enabled: false
},
{
extend: 'selected',
text: 'Enable selected',
enabled: false,
className: 'btn btn-info',
action: function ( e, dt, node, config ) {
const ids = $.map(table.rows('.selected').data(), function (item) {
return item[1]
const ids = $.map(dt.rows({ selected: true }).data(), function (item) {
return item.id;
});
setEnabledForChannels(urlEnable, ids);
setEnabledForChannels(urlEnable, ids, dt);
},
enabled: false
},
{
text: 'Delete selected',
action: function ( e, dt, node, config ) {
const ids = $.map(table.rows('.selected').data(), function (item) {
return item[1]
});
alert(JSON.stringify( ids ));
},
enabled: false
extend: 'selected',
text: 'Delete selected',
enabled: false,
className: 'btn btn-danger',
action: function ( e, dt, node, config ) {
const ids = $.map(dt.rows({ selected: true }).data(), function (item) {
return item.id;
});
if (confirm("Do you really want to delete the " + ids.length + " selected channel(s)?")) {
deleteChannels(ids, dt);
}
},
},
{
extend: 'selectedSingle',
text: 'Edit entry',
enabled: false,
className: 'btn btn-secondary',
action: function ( e, dt, node, config ) {
const id = dt.rows({ selected: true}).data()[0].id;
window.location.href = urlEdit + '/' +id;
},
}
]
});
table.on( 'select deselect', function () {
const selectedRows = table.rows( { selected: true } ).count();
table.button( 2 ).enable( selectedRows > 0 );
table.button( 3 ).enable( selectedRows > 0 );
table.button( 4 ).enable( selectedRows > 0 );
} );
});
const urlEnable = '@Url.Action("EnableChannels","PlaylistApi")';
const urlDisable = '@Url.Action("DisableChannels","PlaylistApi")';
function setEnabledForChannels(url, ids, dataTable) {
const options = {};
options.url = url;
options.type = "PUT";
options.data = JSON.stringify(ids);
options.contentType = "application/json";
options.dataType = "html";
options.success = function (msg) {
dataTable.ajax.reload();
};
options.error = function () {
alert("Error while calling the Web API!");
};
$.ajax(options);
}
function setEnabledForChannels(url, ids) {
const options = {};
options.url = url;
options.type = "PUT";
options.data = JSON.stringify(ids);
options.contentType = "application/json";
options.dataType = "html";
options.success = function (msg) {
$("#msg").html(msg);
};
options.error = function () {
$("#msg").html("Error while calling the Web API!");
};
$.ajax(options);
}
function deleteChannels(ids, dataTable) {
const options = {};
options.url = urlDelete;
options.type = "DELETE";
options.data = JSON.stringify(ids);
options.contentType = "application/json";
options.dataType = "html";
options.success = function (msg) {
dataTable.ajax.reload();
};
options.error = function () {
alert("Error while calling the Web API!");
};
$.ajax(options);
}
</script>
</form>
</form>
@@ -1,65 +0,0 @@
@model Tv7Playlist.Data.PlaylistEntry;
@{
ViewData["Title"] = $"Delete channel {Model.ChannelNumberExport} - {Model.Name}";
}
<div class="row">
<div class="col offset-2 col-8">
<h3>Delete channel @Model.ChannelNumberExport - @Model.Name ?</h3>
</div>
</div>
<hr/>
<form asp-action="DeleteConfirmed">
<input type="hidden" asp-for="Id"/>
<div class="form-group row">
<div class="offset-sm-2 col-sm-2">
<label asp-for="ChannelNumberExport" class="control-label">Channel number export:</label>
</div>
<div class="col-sm-4">
<input asp-for="ChannelNumberExport" readonly class="form-control"/>
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-2">
<label asp-for="EpgMatchName" class="control-label">EPG Name:</label>
</div>
<div class="col-sm-4">
<input asp-for="EpgMatchName" readonly class="form-control"/>
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-2">
<label asp-for="IsAvailable" class="control-label form-check-label">Available</label>
</div>
<div class="col-sm-1 form-check">
<input asp-for="IsAvailable" class="form-control form-control-sm" readonly disabled="true"/>
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-2">
<label asp-for="UrlOriginal" class="control-label">Original URL</label>
</div>
<div class="col-sm-4">
<input asp-for="UrlOriginal" readonly class="form-control"/>
</div>
</div>
<hr/>
<div class="form-group row">
<div class="col offset-sm-4 col-sm-4">
<input type="submit" value="Delete" class="btn btn-danger"/>
<a asp-action="Index" asp-controller="Home" class="btn btn-secondary">Cancel</a>
</div>
</div>
</form>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
+2
View File
@@ -19,11 +19,13 @@
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script src="~/lib/datatables/datatables.js"></script>
<script src="~/lib/moment/moment.js"></script>
</environment>
<environment exclude="Development">
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/datatables/datatables.min.js"></script>
<script src="~/lib/moment/moment.min.js"></script>
</environment>
</head>
<body>
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long