@ -0,0 +1,4 @@ | |||
bin/ | |||
obj/ | |||
/packages/ | |||
/data/ |
@ -0,0 +1,2 @@ | |||
# Default ignored files | |||
/workspace.xml |
@ -0,0 +1,140 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="ContentModelStore"> | |||
<e p="$USER_HOME$/.cache/JetBrains/Rider2020.3/extResources" t="IncludeRecursive" /> | |||
<e p="$USER_HOME$/.cache/JetBrains/Rider2020.3/resharper-host/local/Transient/Rider/v203/SolutionCaches/_c3stream.-187536235.00" t="ExcludeRecursive" /> | |||
<e p="$PROJECT_DIR$" t="IncludeRecursive"> | |||
<e p=".gitignore" t="Include" /> | |||
<e p="LICENSE" t="Include" /> | |||
<e p="Pages" t="Include"> | |||
<e p="Conference.cshtml" t="Include" /> | |||
<e p="Conference.cshtml.cs" t="Include" /> | |||
<e p="Error.cshtml" t="Include" /> | |||
<e p="Error.cshtml.cs" t="Include" /> | |||
<e p="Index.cshtml" t="Include" /> | |||
<e p="Index.cshtml.cs" t="Include" /> | |||
<e p="Info.cshtml" t="Include" /> | |||
<e p="Info.cshtml.cs" t="Include" /> | |||
<e p="Privacy.cshtml" t="Include" /> | |||
<e p="Privacy.cshtml.cs" t="Include" /> | |||
<e p="Shared" t="Include"> | |||
<e p="_Layout.cshtml" t="Include" /> | |||
<e p="_ValidationScriptsPartial.cshtml" t="Include" /> | |||
</e> | |||
<e p="_ViewImports.cshtml" t="Include" /> | |||
<e p="_ViewStart.cshtml" t="Include" /> | |||
</e> | |||
<e p="Properties" t="Include"> | |||
<e p="launchSettings.json" t="Include" /> | |||
</e> | |||
<e p="README.md" t="Include" /> | |||
<e p="Startup.cs" t="Include" /> | |||
<e p="Types.cs" t="Include" /> | |||
<e p="appsettings.Development.json" t="Include" /> | |||
<e p="appsettings.json" t="Include" /> | |||
<e p="bin" t="ExcludeRecursive" /> | |||
<e p="c3stream.cs" t="Include" /> | |||
<e p="c3stream.csproj" t="IncludeRecursive" /> | |||
<e p="c3stream.sln" t="IncludeFlat" /> | |||
<e p="obj" t="ExcludeRecursive"> | |||
<e p="Debug" t="Include"> | |||
<e p="netcoreapp3.1" t="Include"> | |||
<e p="c3stream.AssemblyInfo.cs" t="Include" /> | |||
<e p="c3stream.RazorAssemblyInfo.cs" t="Include" /> | |||
</e> | |||
</e> | |||
</e> | |||
<e p="packages" t="ExcludeRecursive" /> | |||
<e p="wwwroot" t="Include"> | |||
<e p="css" t="Include"> | |||
<e p="fa.css" t="Include" /> | |||
<e p="site.css" t="Include" /> | |||
</e> | |||
<e p="favicon.ico" t="Include" /> | |||
<e p="js" t="Include"> | |||
<e p="site.js" t="Include" /> | |||
</e> | |||
<e p="lib" t="Include"> | |||
<e p="bootstrap" t="Include"> | |||
<e p="LICENSE" t="Include" /> | |||
<e p="dist" t="Include"> | |||
<e p="css" t="Include"> | |||
<e p="bootstrap-grid.css" t="Include" /> | |||
<e p="bootstrap-grid.css.map" t="Include" /> | |||
<e p="bootstrap-grid.min.css" t="Include" /> | |||
<e p="bootstrap-grid.min.css.map" t="Include" /> | |||
<e p="bootstrap-reboot.css" t="Include" /> | |||
<e p="bootstrap-reboot.css.map" t="Include" /> | |||
<e p="bootstrap-reboot.min.css" t="Include" /> | |||
<e p="bootstrap-reboot.min.css.map" t="Include" /> | |||
<e p="bootstrap.css" t="Include" /> | |||
<e p="bootstrap.css.map" t="Include" /> | |||
<e p="bootstrap.min.css" t="Include" /> | |||
<e p="bootstrap.min.css.map" t="Include" /> | |||
</e> | |||
<e p="js" t="Include"> | |||
<e p="bootstrap.bundle.js" t="Include" /> | |||
<e p="bootstrap.bundle.js.map" t="Include" /> | |||
<e p="bootstrap.bundle.min.js" t="Include" /> | |||
<e p="bootstrap.bundle.min.js.map" t="Include" /> | |||
<e p="bootstrap.js" t="Include" /> | |||
<e p="bootstrap.js.map" t="Include" /> | |||
<e p="bootstrap.min.js" t="Include" /> | |||
<e p="bootstrap.min.js.map" t="Include" /> | |||
</e> | |||
</e> | |||
</e> | |||
<e p="jquery" t="Include"> | |||
<e p="LICENSE.txt" t="Include" /> | |||
<e p="dist" t="Include"> | |||
<e p="jquery.js" t="Include" /> | |||
<e p="jquery.min.js" t="Include" /> | |||
<e p="jquery.min.map" t="Include" /> | |||
</e> | |||
</e> | |||
<e p="jquery-validation" t="Include"> | |||
<e p="LICENSE.md" t="Include" /> | |||
<e p="dist" t="Include"> | |||
<e p="additional-methods.js" t="Include" /> | |||
<e p="additional-methods.min.js" t="Include" /> | |||
<e p="jquery.validate.js" t="Include" /> | |||
<e p="jquery.validate.min.js" t="Include" /> | |||
</e> | |||
</e> | |||
<e p="jquery-validation-unobtrusive" t="Include"> | |||
<e p="LICENSE.txt" t="Include" /> | |||
<e p="jquery.validate.unobtrusive.js" t="Include" /> | |||
<e p="jquery.validate.unobtrusive.min.js" t="Include" /> | |||
</e> | |||
</e> | |||
<e p="webfonts" t="Include"> | |||
<e p="fa-brands-400.eot" t="Include" /> | |||
<e p="fa-brands-400.svg" t="Include" /> | |||
<e p="fa-brands-400.ttf" t="Include" /> | |||
<e p="fa-brands-400.woff" t="Include" /> | |||
<e p="fa-brands-400.woff2" t="Include" /> | |||
<e p="fa-duotone-900.eot" t="Include" /> | |||
<e p="fa-duotone-900.svg" t="Include" /> | |||
<e p="fa-duotone-900.ttf" t="Include" /> | |||
<e p="fa-duotone-900.woff" t="Include" /> | |||
<e p="fa-duotone-900.woff2" t="Include" /> | |||
<e p="fa-light-300.eot" t="Include" /> | |||
<e p="fa-light-300.svg" t="Include" /> | |||
<e p="fa-light-300.ttf" t="Include" /> | |||
<e p="fa-light-300.woff" t="Include" /> | |||
<e p="fa-light-300.woff2" t="Include" /> | |||
<e p="fa-regular-400.eot" t="Include" /> | |||
<e p="fa-regular-400.svg" t="Include" /> | |||
<e p="fa-regular-400.ttf" t="Include" /> | |||
<e p="fa-regular-400.woff" t="Include" /> | |||
<e p="fa-regular-400.woff2" t="Include" /> | |||
<e p="fa-solid-900.eot" t="Include" /> | |||
<e p="fa-solid-900.svg" t="Include" /> | |||
<e p="fa-solid-900.ttf" t="Include" /> | |||
<e p="fa-solid-900.woff" t="Include" /> | |||
<e p="fa-solid-900.woff2" t="Include" /> | |||
</e> | |||
</e> | |||
</e> | |||
</component> | |||
</project> |
@ -0,0 +1,4 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> | |||
</project> |
@ -0,0 +1,8 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="ContentModelUserStore"> | |||
<attachedFolders /> | |||
<explicitIncludes /> | |||
<explicitExcludes /> | |||
</component> | |||
</project> |
@ -0,0 +1,6 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="JavaScriptLibraryMappings"> | |||
<file url="PROJECT" libraries="{all, c1a632a160}" /> | |||
</component> | |||
</project> |
@ -0,0 +1,6 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent"> | |||
<option name="ENSURE_MISC_FILE_EXISTS" value="true" /> | |||
</component> | |||
</project> |
@ -0,0 +1,8 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="ProjectModuleManager"> | |||
<modules> | |||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.c3stream/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.c3stream/.idea/riderModule.iml" /> | |||
</modules> | |||
</component> | |||
</project> |
@ -0,0 +1,6 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="RiderProjectSettingsUpdater"> | |||
<option name="vcsConfiguration" value="2" /> | |||
</component> | |||
</project> |
@ -0,0 +1,7 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<module type="RIDER_MODULE" version="4"> | |||
<component name="NewModuleRootManager"> | |||
<content url="file://$MODULE_DIR$/../.." /> | |||
<orderEntry type="sourceFolder" forTests="false" /> | |||
</component> | |||
</module> |
@ -0,0 +1,6 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="VcsDirectoryMappings"> | |||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||
</component> | |||
</project> |
@ -0,0 +1,9 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<module type="RIDER_MODULE" version="4"> | |||
<component name="NewModuleRootManager"> | |||
<content url="file://$MODULE_DIR$/../.." /> | |||
<orderEntry type="sourceFolder" forTests="false" /> | |||
<orderEntry type="library" name="c1a632a160" level="application" /> | |||
<orderEntry type="library" name="all" level="application" /> | |||
</component> | |||
</module> |
@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2020 Laura Hausmann | |||
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. |
@ -0,0 +1,114 @@ | |||
๏ปฟ@page | |||
@model ConferenceModel | |||
@using System.Net | |||
@using static ConferenceModel | |||
@{ | |||
if (c3stream.Conferences.All(c => c.Acronym != Request.Query["c"])) { | |||
Response.Redirect("/"); | |||
return; | |||
} | |||
c3stream.UpdateCookie(Request, Response, $"/Conference?c={Request.Query["c"]}"); | |||
ReadUserData(); | |||
ViewData["Title"] = Request.Query["c"]; | |||
var wc = new WebClient(); | |||
var conference = c3stream.Conferences.First(c => c.Acronym == Request.Query["c"]); | |||
if (conference.Ongoing) { | |||
c3stream.UpdateConference(conference); | |||
} | |||
wc.Dispose(); | |||
} | |||
<table class="table"> | |||
<thead> | |||
<tr> | |||
<th scope="col">Event</th> | |||
<th scope="col"> | |||
@Html.Raw(Request.Query["orderby"] == "published" ? $"<a href=\"/Conference?c={Request.Query["c"]}\">Published" : $"<a href=\"/Conference?c={Request.Query["c"]}&orderby=published\">Date")</th> | |||
<th scope="col">Category</th> | |||
<th scope="col">Title</th> | |||
<th scope="col">Speaker(s)</th> | |||
<th scope="col">Lang</th> | |||
<th scope="col">Actions</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
@foreach (var talk in Request.Query["orderby"] == "published" ? conference.Talks.OrderByDescending(p => p.ReleaseDate) : conference.Talks.OrderBy(p => p.Date)) { | |||
var state = UserData.FirstOrDefault(p => p.TalkId == talk.Guid && p.UserId == Request.Cookies["bookmark"])?.State; | |||
var isWatched = state == "watched"; | |||
var isMarked = state == "marked"; | |||
var file = $"{talk.Slug}.mp4"; | |||
var eventName = talk.Tags.Count <= 1 ? conference.Acronym : talk.Tags[0].Replace("-", "-<br/>"); | |||
var category = talk.Tags.Count switch { | |||
0 => "<no category>", | |||
1 => talk.Tags[0], | |||
2 => "<no category>", | |||
3 => talk.Tags[2], | |||
4 => talk.Tags[3], | |||
5 => talk.Tags[3], | |||
6 => talk.Tags[3], // rc3: is this correct? | |||
_ => "<unknown tag format>" | |||
}; | |||
<tr> | |||
<td>@Html.Raw(eventName)</td> | |||
<td>@(Request.Query["orderby"] == "published" ? talk.ReleaseDate?.Date.ToShortDateString() : talk.Date?.Date.ToShortDateString())</td> | |||
<td>@category</td> | |||
@if (isWatched) { | |||
<td style="color: #95cb7a">@talk.Title</td> | |||
} | |||
else if (isMarked) { | |||
<td style="color: #da7d4f">@talk.Title</td> | |||
} | |||
else { | |||
<td>@talk.Title</td> | |||
} | |||
<td>@(talk.Persons.Any() ? talk.Persons.Aggregate((s, s1) => $"{s}, {s1}") : "<no speakers>")</td> | |||
<td>@talk.OriginalLanguage</td> | |||
<td> | |||
<div class="btn-group" role="group"> | |||
<a href="@talk.FrontendLink.AbsoluteUri" role="button" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Play"> | |||
<i class="fas fa-play-circle"></i> | |||
</a> | |||
@if (System.IO.File.Exists(System.IO.Path.Combine(c3stream.CachePath, conference.Acronym, file))) { | |||
<a href="@(c3stream.CacheUrl + $"{conference.Acronym}/{file}")" role="button" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Mirror"> | |||
<i class="fas fa-cloud-download"></i> | |||
</a> | |||
} | |||
else { | |||
<a href="/" role="button" class="btn btn-primary disabled"> | |||
<i class="fas fa-cloud-download"></i> | |||
</a> | |||
} | |||
<a href="/Info?guid=@talk.Guid" role="button" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Info"> | |||
<i class="fas fa-info-circle"></i> | |||
</a> | |||
@if (isWatched) { | |||
<button onclick="SetState('@talk.Guid', 'unwatched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Mark unwatched"> | |||
<i class="fas fa-times"></i> | |||
</button> | |||
<button class="btn btn-primary disabled"> | |||
<i class="fas fa-clock"></i> | |||
</button> | |||
} | |||
else if (isMarked) { | |||
<button onclick="SetState('@talk.Guid', 'watched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Mark watched"> | |||
<i class="fas fa-check"></i> | |||
</button> | |||
<button onclick="SetState('@talk.Guid', 'unwatched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Remove from watch later"> | |||
<i class="fas fa-undo-alt"></i> | |||
</button> | |||
} | |||
else { | |||
<button onclick="SetState('@talk.Guid', 'watched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Mark watched"> | |||
<i class="fas fa-check"></i> | |||
</button> | |||
<button onclick="SetState('@talk.Guid', 'marked')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="top" title="Add to watch later"> | |||
<i class="fas fa-clock"></i> | |||
</button> | |||
} | |||
</div> | |||
</td> | |||
</tr> | |||
} | |||
</tbody> | |||
</table> |
@ -0,0 +1,58 @@ | |||
๏ปฟusing System.Collections.Generic; | |||
using System.Linq; | |||
using Microsoft.AspNetCore.Mvc.RazorPages; | |||
using Microsoft.Extensions.Logging; | |||
using Newtonsoft.Json; | |||
namespace c3stream.Pages { | |||
public class ConferenceModel : PageModel { | |||
public static List<UserStatus> UserData = new List<UserStatus>(); | |||
private readonly ILogger<ConferenceModel> _logger; | |||
public ConferenceModel(ILogger<ConferenceModel> logger) => _logger = logger; | |||
public void OnGet() { | |||
var guid = Request.Query["guid"]; | |||
var state = Request.Query["state"]; | |||
var userid = Request.Cookies["bookmark"]; | |||
if (string.IsNullOrWhiteSpace(guid) || string.IsNullOrWhiteSpace(state) || !Request.Cookies.ContainsKey("bookmark")) | |||
return; | |||
lock (c3stream.Lock) { | |||
ReadUserData(); | |||
var existing = UserData.FirstOrDefault(p => p.TalkId == guid && p.UserId == userid); | |||
if (existing != null) | |||
if (state == "unwatched") | |||
UserData.Remove(existing); | |||
else | |||
existing.State = state; | |||
else | |||
UserData.Add(new UserStatus(userid, guid, state)); | |||
WriteUserData(); | |||
Response.Redirect("/"); | |||
} | |||
} | |||
public static void ReadUserData() { | |||
lock (c3stream.Lock) | |||
UserData = JsonConvert.DeserializeObject<List<UserStatus>>(System.IO.File.ReadAllText(c3stream.DbPath)); | |||
} | |||
public static void WriteUserData() { | |||
lock (c3stream.Lock) | |||
System.IO.File.WriteAllText(c3stream.DbPath, JsonConvert.SerializeObject(UserData)); | |||
} | |||
public class UserStatus { | |||
public readonly string TalkId; | |||
public readonly string UserId; | |||
public string State; | |||
public UserStatus(string userId, string talkId, string state = "unwatched") { | |||
UserId = userId; | |||
State = state; | |||
TalkId = talkId; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,25 @@ | |||
๏ปฟ@page | |||
@model ErrorModel | |||
@{ | |||
ViewData["Title"] = "Error"; | |||
} | |||
<h1 class="text-danger">Error.</h1> | |||
<h2 class="text-danger">An error occurred while processing your request.</h2> | |||
@if (Model.ShowRequestId) { | |||
<p> | |||
<strong>Request ID:</strong> <code>@Model.RequestId</code> | |||
</p> | |||
} | |||
<h3>Development Mode</h3> | |||
<p> | |||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred. | |||
</p> | |||
<p> | |||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong> | |||
It can result in displaying sensitive information from exceptions to end users. | |||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong> | |||
and restarting the app. | |||
</p> |
@ -0,0 +1,21 @@ | |||
using System.Diagnostics; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.AspNetCore.Mvc.RazorPages; | |||
using Microsoft.Extensions.Logging; | |||
namespace c3stream.Pages { | |||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] | |||
public class ErrorModel : PageModel { | |||
private readonly ILogger<ErrorModel> _logger; | |||
public ErrorModel(ILogger<ErrorModel> logger) => _logger = logger; | |||
public string RequestId { get; set; } | |||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); | |||
public void OnGet() { | |||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; | |||
} | |||
} | |||
} |
@ -0,0 +1,17 @@ | |||
๏ปฟ@page | |||
@model IndexModel | |||
@{ | |||
ViewData["Title"] = "Home"; | |||
c3stream.UpdateCookie(Request, Response, "/"); | |||
} | |||
<div style="text-align: center"> | |||
<h1>Welcome to c3stream!</h1> | |||
Your bookmark link:<br/> | |||
<code onclick="copyToClipboard(this)">https://@Request.Host.Value?bookmark=@Request.Cookies["bookmark"]</code><br/><br/> | |||
<div class="btn-group"> | |||
@foreach (var conf in c3stream.Conferences) { | |||
<a role="button" class="btn btn-primary" href="/Conference?c=@conf.Acronym">@conf.Acronym</a> | |||
} | |||
</div> | |||
</div> |
@ -0,0 +1,12 @@ | |||
๏ปฟusing Microsoft.AspNetCore.Mvc.RazorPages; | |||
using Microsoft.Extensions.Logging; | |||
namespace c3stream.Pages { | |||
public class IndexModel : PageModel { | |||
private readonly ILogger<IndexModel> _logger; | |||
public IndexModel(ILogger<IndexModel> logger) => _logger = logger; | |||
public void OnGet() { } | |||
} | |||
} |
@ -0,0 +1,114 @@ | |||
๏ปฟ@page | |||
@model InfoModel | |||
@{ | |||
ViewData["Title"] = "Info"; | |||
} | |||
@{ | |||
if (string.IsNullOrWhiteSpace(Request.Query["guid"]) || c3stream.GetEventByGuid(Request.Query["guid"]) == null) { | |||
Response.Redirect("/"); | |||
return; | |||
} | |||
c3stream.UpdateCookie(Request, Response, $"/Info?guid={Request.Query["guid"]}"); | |||
ConferenceModel.ReadUserData(); | |||
var talk = c3stream.GetEventByGuid(Request.Query["guid"]); | |||
var state = ConferenceModel.UserData.FirstOrDefault(p => p.TalkId == Request.Query["guid"] && p.UserId == Request.Cookies["bookmark"])?.State; | |||
if (talk == null) { | |||
Response.Redirect("/"); | |||
return; | |||
} | |||
if (state == null) { | |||
state = "unwatched"; | |||
} | |||
var title = talk.Title; | |||
var speakers = talk.Persons.Any() ? talk.Persons.Aggregate((s, s1) => $"{s}, {s1}") : "<no speakers>"; | |||
var description = talk.Description; | |||
if (string.IsNullOrEmpty(description)) { | |||
description = "<missing description>"; | |||
} | |||
var isWatched = state == "watched"; | |||
var isMarked = state == "marked"; | |||
var file = $"{talk.Slug}.mp4"; | |||
var conference = c3stream.GetConferenceByEventGuid(talk.Guid); | |||
var eventName = talk.Tags.Count <= 1 ? conference.Acronym : talk.Tags[0]; | |||
var logoPath = System.IO.Path.Combine(c3stream.CachePath, conference.Acronym, "logo.png"); | |||
var category = talk.Tags.Count switch { | |||
0 => "<no category>", | |||
1 => talk.Tags[0], | |||
2 => "<no category>", | |||
3 => talk.Tags[2], | |||
4 => talk.Tags[3], | |||
5 => talk.Tags[3], | |||
_ => "<unknown tag format>" | |||
}; | |||
} | |||
@if (System.IO.File.Exists(logoPath)) { | |||
<img src="@(c3stream.CacheUrl + $"{conference.Acronym}/logo.png")" alt="Conference logo" style="max-height: 110px; float: right;"/> | |||
} | |||
else { | |||
<img src="@conference.LogoUri" alt="Conference logo" style="max-height: 110px; float: right;"/> | |||
} | |||
@if (isWatched) { | |||
<h3 style="color: #95cb7a">@title - <i>@speakers</i></h3> | |||
} | |||
else if (isMarked) { | |||
<h3 style="color: #da7d4f">@title - <i>@speakers</i></h3> | |||
} | |||
else { | |||
<h3>@title - <i>@speakers</i></h3> | |||
} | |||
<h5>@eventName - @category - @talk.Date?.Date.ToShortDateString()</h5> | |||
<div class="btn-group" role="group" style="margin-bottom: 10px"> | |||
<a href="@talk.FrontendLink.AbsoluteUri" role="button" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="right" title="Play"> | |||
<i class="fas fa-play-circle"></i> | |||
</a> | |||
@if (System.IO.File.Exists(System.IO.Path.Combine(c3stream.CachePath, conference.Acronym, file))) { | |||
<a href="@(c3stream.CacheUrl + $"{conference.Acronym}/{file}")" role="button" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="right" title="Mirror"> | |||
<i class="fas fa-cloud-download"></i> | |||
</a> | |||
} | |||
else { | |||
<a href="/" role="button" class="btn btn-primary disabled"> | |||
<i class="fas fa-cloud-download"></i> | |||
</a> | |||
} | |||
@if (isWatched) { | |||
<button onclick="SetState('@talk.Guid', 'unwatched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="left" title="Mark unwatched"> | |||
<i class="fas fa-times"></i> | |||
</button> | |||
<button class="btn btn-primary disabled"> | |||
<i class="fas fa-clock"></i> | |||
</button> | |||
} | |||
else if (isMarked) { | |||
<button onclick="SetState('@talk.Guid', 'watched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="left" title="Mark watched"> | |||
<i class="fas fa-check"></i> | |||
</button> | |||
<button onclick="SetState('@talk.Guid', 'unwatched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="left" title="Remove from watch later"> | |||
<i class="fas fa-undo-alt"></i> | |||
</button> | |||
} | |||
else { | |||
<button onclick="SetState('@talk.Guid', 'watched')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="left" title="Mark watched"> | |||
<i class="fas fa-check"></i> | |||
</button> | |||
<button onclick="SetState('@talk.Guid', 'marked')" class="btn btn-primary w-100" data-toggle="tooltip" data-placement="left" title="Add to watch later"> | |||
<i class="fas fa-clock"></i> | |||
</button> | |||
} | |||
</div> | |||
<p style="text-align: justify"> | |||
@Html.Raw(description.Replace("\n", "<br/>").Replace("<p>", "").Replace("</p>", "")) | |||
</p> | |||
Share this talk:<br/> | |||
<code onclick="copyToClipboard(this)">https://@Request.Host.Value/Info?guid=@Request.Query["guid"]</code> |
@ -0,0 +1,12 @@ | |||
๏ปฟusing Microsoft.AspNetCore.Mvc.RazorPages; | |||
using Microsoft.Extensions.Logging; | |||
namespace c3stream.Pages { | |||
public class InfoModel : PageModel { | |||
private readonly ILogger<InfoModel> _logger; | |||
public InfoModel(ILogger<InfoModel> logger) => _logger = logger; | |||
public void OnGet() { } | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
๏ปฟ@page | |||
@model PrivacyModel | |||
@{ | |||
ViewData["Title"] = "Privacy"; | |||
} | |||
<h3>Privacy</h3> | |||
<p style="text-align: justify"> | |||
All data saved about you on this website is the watched status of talks you marked in association with your randomly generated bookmark UUID. | |||
No logs are kept, no trackers used. Keep in mind that you are forwarded to media.ccc.de when you watch a talk, and therefore should check their privacy policy as well. <br/> | |||
Have fun! | |||
</p> |
@ -0,0 +1,12 @@ | |||
๏ปฟusing Microsoft.AspNetCore.Mvc.RazorPages; | |||
using Microsoft.Extensions.Logging; | |||
namespace c3stream.Pages { | |||
public class PrivacyModel : PageModel { | |||
private readonly ILogger<PrivacyModel> _logger; | |||
public PrivacyModel(ILogger<PrivacyModel> logger) => _logger = logger; | |||
public void OnGet() { } | |||
} | |||
} |
@ -0,0 +1,44 @@ | |||
๏ปฟ<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"/> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |||
<title>@ViewData["Title"] - c3stream</title> | |||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | |||
<link rel="stylesheet" href="~/css/fa.css" crossorigin="anonymous"> | |||
<link rel="stylesheet" href="~/css/site.css"/> | |||
</head> | |||
<body> | |||
<header> | |||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> | |||
<div class="container-fluid" style="width: 90%"> | |||
<a class="navbar-brand" asp-area="" asp-page="/Index">c3stream <small style="font-size: x-small">v5</small></a> | |||
<button class="navbar-toggler" role="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent" | |||
aria-expanded="false" aria-label="Toggle navigation"> | |||
<span class="navbar-toggler-icon"></span> | |||
</button> | |||
</div> | |||
</nav> | |||
</header> | |||
<div class="container-fluid" style="width: 90%"> | |||
<main role="main" class="pb-3"> | |||
@RenderBody() | |||
</main> | |||
</div> | |||
<footer class="border-top footer"> | |||
<div class="container-fluid" style="width: 90%; text-align: center"> | |||
<a href="/Privacy">Privacy</a> - | |||
<a href="mailto:c3stream-contact@zotan.pw">Contact</a> - | |||
<a href="https://git.zotan.services/zotan/c3stream/">Source Code</a> - | |||
c3stream is not affiliated with media.ccc.de in any way. Mirrored video files display their license in the video, no rights reserved. | |||
</div> | |||
</footer> | |||
<script src="~/lib/jquery/dist/jquery.min.js"></script> | |||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> | |||
<script src="~/js/site.js" asp-append-version="true"></script> | |||
@RenderSection("Scripts", false) | |||
</body> | |||
</html> |
@ -0,0 +1,2 @@ | |||
๏ปฟ<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> | |||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> |
@ -0,0 +1,3 @@ | |||
@using global::c3stream | |||
@namespace c3stream.Pages | |||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
@ -0,0 +1,3 @@ | |||
๏ปฟ@{ | |||
Layout = "_Layout"; | |||
} |
@ -0,0 +1,20 @@ | |||
๏ปฟ{ | |||
"iisSettings": { | |||
"windowsAuthentication": false, | |||
"anonymousAuthentication": true, | |||
"iisExpress": { | |||
"applicationUrl": "http://localhost:37898", | |||
"sslPort": 44314 | |||
} | |||
}, | |||
"profiles": { | |||
"c3stream": { | |||
"commandName": "Project", | |||
"launchBrowser": false, | |||
"applicationUrl": "http://localhost:5000", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1 @@ | |||
c3stream is a small proxy site meant for saving watched status & watch-later-lists for media.ccc.de talks. Test in production at https://c3stream.de |
@ -0,0 +1,35 @@ | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.Extensions.Configuration; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.Hosting; | |||
namespace c3stream { | |||
public class Startup { | |||
public Startup(IConfiguration configuration) => Configuration = configuration; | |||
public IConfiguration Configuration { get; } | |||
// This method gets called by the runtime. Use this method to add services to the container. | |||
public void ConfigureServices(IServiceCollection services) { | |||
services.AddRazorPages(); | |||
} | |||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { | |||
if (env.IsDevelopment()) | |||
app.UseDeveloperExceptionPage(); | |||
else | |||
app.UseExceptionHandler("/Error"); | |||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. | |||
app.UseStaticFiles(); | |||
app.UseRouting(); | |||
app.UseAuthorization(); | |||
app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); | |||
} | |||
} | |||
} |
@ -0,0 +1,276 @@ | |||
// <auto-generated /> | |||
// | |||
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: | |||
// | |||
// using c3stream; | |||
// | |||
// var conference = Conference.FromJson(jsonString); | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Globalization; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Converters; | |||
namespace c3stream { | |||
public partial class Conference { | |||
[JsonProperty("acronym", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Acronym { get; set; } | |||
[JsonProperty("aspect_ratio", NullValueHandling = NullValueHandling.Ignore)] | |||
public string AspectRatio { get; set; } | |||
[JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? UpdatedAt { get; set; } | |||
[JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Title { get; set; } | |||
[JsonProperty("schedule_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public string ScheduleUrl { get; set; } | |||
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Slug { get; set; } | |||
[JsonProperty("event_last_released_at", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? EventLastReleasedAt { get; set; } | |||
[JsonProperty("webgen_location", NullValueHandling = NullValueHandling.Ignore)] | |||
public string WebgenLocation { get; set; } | |||
[JsonProperty("logo_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri LogoUrl { get; set; } | |||
[JsonProperty("images_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ImagesUrl { get; set; } | |||
[JsonProperty("recordings_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri RecordingsUrl { get; set; } | |||
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri Url { get; set; } | |||
[JsonProperty("events", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<Event> Events { get; set; } | |||
} | |||
public class Event { | |||
[JsonProperty("guid", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Guid { get; set; } | |||
[JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Title { get; set; } | |||
[JsonProperty("subtitle")] public string Subtitle { get; set; } | |||
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Slug { get; set; } | |||
[JsonProperty("link", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri Link { get; set; } | |||
[JsonProperty("description")] public string Description { get; set; } | |||
[JsonProperty("original_language", NullValueHandling = NullValueHandling.Ignore)] | |||
public string OriginalLanguage { get; set; } | |||
[JsonProperty("persons", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<string> Persons { get; set; } | |||
[JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<string> Tags { get; set; } | |||
[JsonProperty("view_count", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? ViewCount { get; set; } | |||
[JsonProperty("promoted", NullValueHandling = NullValueHandling.Ignore)] | |||
public bool? Promoted { get; set; } | |||
[JsonProperty("date", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? Date { get; set; } | |||
[JsonProperty("release_date", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? ReleaseDate { get; set; } | |||
[JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? UpdatedAt { get; set; } | |||
[JsonProperty("length", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Length { get; set; } | |||
[JsonProperty("duration", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Duration { get; set; } | |||
[JsonProperty("thumb_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ThumbUrl { get; set; } | |||
[JsonProperty("poster_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri PosterUrl { get; set; } | |||
[JsonProperty("timeline_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri TimelineUrl { get; set; } | |||
[JsonProperty("thumbnails_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ThumbnailsUrl { get; set; } | |||
[JsonProperty("frontend_link", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri FrontendLink { get; set; } | |||
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri Url { get; set; } | |||
[JsonProperty("conference_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ConferenceUrl { get; set; } | |||
[JsonProperty("related", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<object> Related { get; set; } | |||
} | |||
public partial class Conference { | |||
public static Conference FromJson(string json) => JsonConvert.DeserializeObject<Conference>(json, Converter.Settings); | |||
} | |||
public static partial class Serialize { | |||
public static string ToJson(this Conference self) => JsonConvert.SerializeObject(self, Converter.Settings); | |||
} | |||
internal static class Converter { | |||
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { | |||
MetadataPropertyHandling = MetadataPropertyHandling.Ignore, | |||
DateParseHandling = DateParseHandling.None, | |||
Converters = {new IsoDateTimeConverter {DateTimeStyles = DateTimeStyles.AssumeUniversal}} | |||
}; | |||
} | |||
public partial class Talk { | |||
[JsonProperty("guid", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Guid { get; set; } | |||
[JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Title { get; set; } | |||
[JsonProperty("subtitle", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Subtitle { get; set; } | |||
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Slug { get; set; } | |||
[JsonProperty("link", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri Link { get; set; } | |||
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Description { get; set; } | |||
[JsonProperty("original_language", NullValueHandling = NullValueHandling.Ignore)] | |||
public string OriginalLanguage { get; set; } | |||
[JsonProperty("persons", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<string> Persons { get; set; } | |||
[JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<string> Tags { get; set; } | |||
[JsonProperty("view_count", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? ViewCount { get; set; } | |||
[JsonProperty("promoted", NullValueHandling = NullValueHandling.Ignore)] | |||
public bool? Promoted { get; set; } | |||
[JsonProperty("date", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? Date { get; set; } | |||
[JsonProperty("release_date", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? ReleaseDate { get; set; } | |||
[JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? UpdatedAt { get; set; } | |||
[JsonProperty("length", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Length { get; set; } | |||
[JsonProperty("duration", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Duration { get; set; } | |||
[JsonProperty("thumb_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ThumbUrl { get; set; } | |||
[JsonProperty("poster_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri PosterUrl { get; set; } | |||
[JsonProperty("timeline_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri TimelineUrl { get; set; } | |||
[JsonProperty("thumbnails_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ThumbnailsUrl { get; set; } | |||
[JsonProperty("frontend_link", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri FrontendLink { get; set; } | |||
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri Url { get; set; } | |||
[JsonProperty("conference_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ConferenceUrl { get; set; } | |||
[JsonProperty("related", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<object> Related { get; set; } | |||
[JsonProperty("recordings", NullValueHandling = NullValueHandling.Ignore)] | |||
public List<Recording> Recordings { get; set; } | |||
} | |||
public class Recording { | |||
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Size { get; set; } | |||
[JsonProperty("length", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Length { get; set; } | |||
[JsonProperty("mime_type", NullValueHandling = NullValueHandling.Ignore)] | |||
public string MimeType { get; set; } | |||
[JsonProperty("language", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Language { get; set; } | |||
[JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Filename { get; set; } | |||
[JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] | |||
public string State { get; set; } | |||
[JsonProperty("folder", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Folder { get; set; } | |||
[JsonProperty("high_quality", NullValueHandling = NullValueHandling.Ignore)] | |||
public bool? HighQuality { get; set; } | |||
[JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Width { get; set; } | |||
[JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] | |||
public long? Height { get; set; } | |||
[JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] | |||
public DateTimeOffset? UpdatedAt { get; set; } | |||
[JsonProperty("recording_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri RecordingUrl { get; set; } | |||
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri Url { get; set; } | |||
[JsonProperty("event_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri EventUrl { get; set; } | |||
[JsonProperty("conference_url", NullValueHandling = NullValueHandling.Ignore)] | |||
public Uri ConferenceUrl { get; set; } | |||
} | |||
public partial class Talk { | |||
public static Talk FromJson(string json) => JsonConvert.DeserializeObject<Talk>(json, Converter.Settings); | |||
} | |||
public static partial class Serialize { | |||
public static string ToJson(this Talk self) => JsonConvert.SerializeObject(self, Converter.Settings); | |||
} | |||
} |
@ -0,0 +1,9 @@ | |||
{ | |||
"Logging": { | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft": "Warning", | |||
"Microsoft.Hosting.Lifetime": "Information" | |||
} | |||
} | |||
} |
@ -0,0 +1,10 @@ | |||
{ | |||
"Logging": { | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft": "Warning", | |||
"Microsoft.Hosting.Lifetime": "Information" | |||
} | |||
}, | |||
"AllowedHosts": "*" | |||
} |
@ -0,0 +1,122 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Net; | |||
using c3stream.Pages; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.Extensions.Hosting; | |||
namespace c3stream { | |||
public static class c3stream { | |||
public const string DataPath = "data"; | |||
public const string DbFile = "c3stream.user.json"; | |||
public const string CachePath = "/mnt/storage/archive/Video/congress/"; | |||
public const string CacheUrl = "https://c3stream-mirror.zotan.services/"; | |||
public static object Lock = new object(); | |||
public static string DbPath = Path.Combine(DataPath, DbFile); | |||
public static List<ConferenceObject> Conferences = new List<ConferenceObject> { | |||
new ConferenceObject("rc3", true), | |||
new ConferenceObject("36c3"), | |||
new ConferenceObject("camp2019"), | |||
new ConferenceObject("gpn19"), | |||
new ConferenceObject("35c3"), | |||
new ConferenceObject("34c3"), | |||
new ConferenceObject("33c3"), | |||
new ConferenceObject("32c3"), | |||
new ConferenceObject("31c3"), | |||
new ConferenceObject("30c3") | |||
}; | |||
public static void Main(string[] args) { | |||
if (!Directory.Exists(DataPath)) | |||
Directory.CreateDirectory(DataPath); | |||
if (!File.Exists(DbPath)) | |||
ConferenceModel.WriteUserData(); | |||
foreach (var conference in Conferences) | |||
UpdateConference(conference); | |||
if (args.Length != 0) { | |||
if (args[0] == "logo") | |||
foreach (var conference in Conferences) { | |||
Console.WriteLine($"wget {conference.LogoUri} -O {Path.Combine(CachePath, conference.Acronym, "logo.png")}"); | |||
} | |||
else if (Conferences.All(p => p.Acronym != args[0])) | |||
Console.WriteLine("No matching conference found."); | |||
else | |||
foreach (var talk in Conferences.First(p => p.Acronym == args[0]).Talks) | |||
Console.WriteLine($"youtube-dl -f \"best[ext = mp4]\" {talk.FrontendLink} -o \"{Path.Combine(CachePath, args[0], talk.Slug)}.mp4\""); | |||
} | |||
else { | |||
CreateHostBuilder(args).Build().Run(); | |||
} | |||
} | |||
public static void UpdateConference(ConferenceObject conference) { | |||
using var wc = new WebClient(); | |||
var jsonpath = Path.Combine(DataPath, conference.Acronym + "_index.json"); | |||
var json = ""; | |||
if (!File.Exists(jsonpath)) { | |||
json = wc.DownloadString($"https://api.media.ccc.de/public/conferences/{conference.Acronym}"); | |||
File.WriteAllText(jsonpath, json); | |||
} | |||
else if (conference.Ongoing) { | |||
json = wc.DownloadString($"https://api.media.ccc.de/public/conferences/{conference.Acronym}"); | |||
} | |||
else { | |||
json = File.ReadAllText(jsonpath); | |||
} | |||
var parsed = Conference.FromJson(json); | |||
lock (Lock) { | |||
conference.Talks.Clear(); | |||
conference.LogoUri = parsed.LogoUrl.AbsoluteUri; | |||
conference.Talks.AddRange(parsed.Events); | |||
conference.Talks.ForEach(p => p.Guid = p.Guid.Trim()); | |||
} | |||
} | |||
public static void UpdateCookie(HttpRequest request, HttpResponse response, string redirectUri) { | |||
//if new bookmark is in uri | |||
if (request.Query.ContainsKey("bookmark") && Guid.TryParseExact(request.Query["bookmark"], "D", out _)) { | |||
response.Cookies.Append("bookmark", request.Query["bookmark"], new CookieOptions {Expires = DateTimeOffset.MaxValue}); | |||
} | |||
//if no cookie exists or cookie is invalid | |||
else if (!request.Cookies.ContainsKey("bookmark") || !Guid.TryParseExact(request.Cookies["bookmark"], "D", out _)) { | |||
var guid = Guid.NewGuid().ToString(); | |||
response.Cookies.Append("bookmark", guid, new CookieOptions {Expires = DateTimeOffset.MaxValue}); | |||
} | |||
if (request.Query.ContainsKey("bookmark")) { | |||
response.Redirect(redirectUri); | |||
} | |||
} | |||
public static Event GetEventByGuid(string guid) { | |||
return Conferences.SelectMany(c => c.Talks.Where(talk => talk.Guid.ToString() == guid)).FirstOrDefault(); | |||
} | |||
public static ConferenceObject GetConferenceByEventGuid(string guid) { | |||
return Conferences.FirstOrDefault(c => c.Talks.Any(t => t.Guid.ToString() == guid)); | |||
} | |||
public static IHostBuilder CreateHostBuilder(string[] args) => | |||
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); | |||
public class ConferenceObject { | |||
public string Acronym; | |||
public bool Ongoing; | |||
public string LogoUri; | |||
public List<Event> Talks = new List<Event>(); | |||
public ConferenceObject(string acronym, bool ongoing = false) { | |||
Acronym = acronym; | |||
Ongoing = ongoing; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,36 @@ | |||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<_ContentIncludedByDefault Remove="data\database.json" /> | |||
<_ContentIncludedByDefault Remove="data\_c3stream.json" /> | |||
<_ContentIncludedByDefault Remove="data\33c3.json" /> | |||
<_ContentIncludedByDefault Remove="data\34c3.json" /> | |||
<_ContentIncludedByDefault Remove="data\35c3.json" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Compile Remove="data\**" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<EmbeddedResource Remove="data\**" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Remove="data\**" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Content Remove="data\**" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,4 @@ | |||
๏ปฟ<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | |||
<s:String x:Key="/Default/CodeInspection/WebPathMapping/IgnoredPaths/=WWWROOT_005C_0020_002B_0020FILE_002EFILENAME/@EntryIndexedValue">wwwroot\ + file.Filename</s:String> | |||
<s:String x:Key="/Default/CodeInspection/WebPathMapping/PathsInCorrectCasing/=WWWROOT_005C_0020_002B_0020FILE_002EFILENAME/@EntryIndexedValue">wwwroot\ + file.Filename</s:String></wpf:ResourceDictionary> |
@ -0,0 +1,16 @@ | |||
๏ปฟ | |||
Microsoft Visual Studio Solution File, Format Version 12.00 | |||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "c3stream", "c3stream.csproj", "{BC6A24FE-B35F-4F0A-8E8E-221E61E41EEF}" | |||
EndProject | |||
Global | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
Debug|Any CPU = Debug|Any CPU | |||
Release|Any CPU = Release|Any CPU | |||
EndGlobalSection | |||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||
{BC6A24FE-B35F-4F0A-8E8E-221E61E41EEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
{BC6A24FE-B35F-4F0A-8E8E-221E61E41EEF}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
{BC6A24FE-B35F-4F0A-8E8E-221E61E41EEF}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
{BC6A24FE-B35F-4F0A-8E8E-221E61E41EEF}.Release|Any CPU.Build.0 = Release|Any CPU | |||
EndGlobalSection | |||
EndGlobal |
@ -0,0 +1,110 @@ | |||
๏ปฟ/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification | |||
for details on configuring this project to bundle and minify static web assets. */ | |||
a.navbar-brand { | |||
color: #e3d7c0 !important; | |||
white-space: normal; | |||
text-align: center; | |||
word-break: break-all; | |||
} | |||
a:not(.btn):not(.navbar-brand) { | |||
color: #7ca9c8 !important; | |||
} | |||
.navbar { | |||
color: #ebddc4 !important; | |||
background-color: #1b1b18 !important; | |||
border-color: #3c3d3c !important; | |||
} | |||
/* Sticky footer styles | |||
-------------------------------------------------- */ | |||
html { | |||
font-size: 14px; | |||
} | |||
@media (min-width: 768px) { | |||
html { | |||
font-size: 16px; | |||
} | |||
} | |||
.box-shadow { | |||
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); | |||
} | |||
button.accept-policy { | |||
font-size: 1rem; | |||
line-height: inherit; | |||
} | |||
/* Sticky footer styles | |||
-------------------------------------------------- */ | |||
html { | |||
position: relative; | |||
min-height: 100%; | |||
} | |||
body { | |||
/* Margin bottom by footer height */ | |||
margin-bottom: 60px; | |||
color: #ebddc4 !important; | |||
background-color: #1b1b18 !important; | |||
} | |||
.table { | |||
color: #ebddc4 !important; | |||
} | |||
.footer { | |||
position: absolute; | |||
bottom: 0; | |||
width: 100%; | |||
white-space: nowrap; | |||
line-height: 60px; /* Vertically center the text there */ | |||
} | |||
.table td { | |||
vertical-align: middle; | |||
border-color: #3c3d3c !important; | |||
} | |||
.table th { | |||
vertical-align: middle; | |||
border-color: #3c3d3c !important; | |||
} | |||
.btn-primary.disabled { | |||
color: #afafa2; | |||
border-color: #3c6385; | |||
background-color: #254667 !important; | |||
opacity: 1; | |||
border-top-color: #254667 !important; | |||
border-bottom-color: #254667 !important; | |||
} | |||
.btn-primary { | |||
color: #ffffff; | |||
border-color: #3c6385; | |||
background-color: #375a7a; | |||
width: 42px !important; | |||
} | |||
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { | |||
color: #ffffff; | |||
border-color: #3c6385; | |||
background-color: #2c5f93; | |||
} | |||
.border-top { | |||
border-top-color: #3c3d3c !important; | |||
} | |||
code { | |||
color: #cB6d87 !important; | |||
} | |||
.fa-times { | |||
} |
@ -0,0 +1,26 @@ | |||
๏ปฟ// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification | |||
// for details on configuring this project to bundle and minify static web assets. | |||
// Write your Javascript code. | |||
function SetState(guid, state) { | |||
$.get("/Conference?state=" + state + "&guid=" + guid, function (data, status) { | |||
location.reload(); | |||
}); | |||
} | |||
function copyToClipboard(field) { | |||
let textarea = document.createElement('textarea'); | |||
textarea.id = 't'; | |||
textarea.style.height = "0"; | |||
document.body.appendChild(textarea); | |||
textarea.value = field.innerText; | |||
let selector = document.querySelector('#t'); | |||
selector.select(); | |||
document.execCommand('copy'); | |||
document.body.removeChild(textarea); | |||
} | |||
$(function () { | |||
$('[data-toggle="tooltip"]').tooltip() | |||
}); |
@ -0,0 +1,22 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2011-2018 Twitter, Inc. | |||
Copyright (c) 2011-2018 The Bootstrap Authors | |||
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. |
@ -0,0 +1,331 @@ | |||
/*! | |||
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) | |||
* Copyright 2011-2019 The Bootstrap Authors | |||
* Copyright 2011-2019 Twitter, Inc. | |||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) | |||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | |||
*/ | |||
*, | |||
*::before, | |||
*::after { | |||
box-sizing: border-box; | |||
} | |||
html { | |||
font-family: sans-serif; | |||
line-height: 1.15; | |||
-webkit-text-size-adjust: 100%; | |||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||
} | |||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { | |||
display: block; | |||
} | |||
body { | |||
margin: 0; | |||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | |||
font-size: 1rem; | |||
font-weight: 400; | |||
line-height: 1.5; | |||
color: #212529; | |||
text-align: left; | |||
background-color: #fff; | |||
} | |||
[tabindex="-1"]:focus { | |||
outline: 0 !important; | |||
} | |||
hr { | |||
box-sizing: content-box; | |||
height: 0; | |||
overflow: visible; | |||
} | |||
h1, h2, h3, h4, h5, h6 { | |||
margin-top: 0; | |||
margin-bottom: 0.5rem; | |||
} | |||
p { | |||
margin-top: 0; | |||
margin-bottom: 1rem; | |||
} | |||
abbr[title], | |||
abbr[data-original-title] { | |||
text-decoration: underline; | |||
-webkit-text-decoration: underline dotted; | |||
text-decoration: underline dotted; | |||
cursor: help; | |||
border-bottom: 0; | |||
-webkit-text-decoration-skip-ink: none; | |||
text-decoration-skip-ink: none; | |||
} | |||
address { | |||
margin-bottom: 1rem; | |||
font-style: normal; | |||
line-height: inherit; | |||
} | |||
ol, | |||
ul, | |||
dl { | |||
margin-top: 0; | |||
margin-bottom: 1rem; | |||
} | |||
ol ol, | |||
ul ul, | |||
ol ul, | |||
ul ol { | |||
margin-bottom: 0; | |||
} | |||
dt { | |||
font-weight: 700; | |||
} | |||
dd { | |||
margin-bottom: .5rem; | |||
margin-left: 0; | |||
} | |||
blockquote { | |||
margin: 0 0 1rem; | |||
} | |||
b, | |||
strong { | |||
font-weight: bolder; | |||
} | |||
small { | |||
font-size: 80%; | |||
} | |||
sub, | |||
sup { | |||
position: relative; | |||
font-size: 75%; | |||
line-height: 0; | |||
vertical-align: baseline; | |||
} | |||
sub { | |||
bottom: -.25em; | |||
} | |||
sup { | |||
top: -.5em; | |||
} | |||
a { | |||
color: #007bff; | |||
text-decoration: none; | |||
background-color: transparent; | |||
} | |||
a:hover { | |||
color: #0056b3; | |||
text-decoration: underline; | |||
} | |||
a:not([href]):not([tabindex]) { | |||
color: inherit; | |||
text-decoration: none; | |||
} | |||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { | |||
color: inherit; | |||
text-decoration: none; | |||
} | |||
a:not([href]):not([tabindex]):focus { | |||
outline: 0; | |||
} | |||
pre, | |||
code, | |||
kbd, | |||
samp { | |||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |||
font-size: 1em; | |||
} | |||
pre { | |||
margin-top: 0; | |||
margin-bottom: 1rem; | |||
overflow: auto; | |||
} | |||
figure { | |||
margin: 0 0 1rem; | |||
} | |||
img { | |||
vertical-align: middle; | |||
border-style: none; | |||
} | |||
svg { | |||
overflow: hidden; | |||
vertical-align: middle; | |||
} | |||
table { | |||
border-collapse: collapse; | |||
} | |||
caption { | |||
padding-top: 0.75rem; | |||
padding-bottom: 0.75rem; | |||
color: #6c757d; | |||
text-align: left; | |||
caption-side: bottom; | |||
} | |||
th { | |||
text-align: inherit; | |||
} | |||
label { | |||
display: inline-block; | |||
margin-bottom: 0.5rem; | |||
} | |||
button { | |||
border-radius: 0; | |||
} | |||
button:focus { | |||
outline: 1px dotted; | |||
outline: 5px auto -webkit-focus-ring-color; | |||
} | |||
input, | |||
button, | |||
select, | |||
optgroup, | |||
textarea { | |||
margin: 0; | |||
font-family: inherit; | |||
font-size: inherit; | |||
line-height: inherit; | |||
} | |||
button, | |||
input { | |||
overflow: visible; | |||
} | |||
button, | |||
select { | |||