Compare commits
30 commits
Author | SHA1 | Date | |
---|---|---|---|
Leah | ab04b91bff | ||
Leah | 7d750777ec | ||
Leah | bb01d955af | ||
Isabelle | 6a8708bcf8 | ||
Isabelle | acac39a90c | ||
Leah | d9bed3ac47 | ||
Leah | db8b55cadc | ||
Leah | b59a5633f0 | ||
Leah | faeb30325f | ||
Leah | 0d789a5e11 | ||
Leah | e5ea274135 | ||
Leah | 6d0e9c999d | ||
Leah | 155e7c1bbb | ||
Leah | 4b6e69d4c1 | ||
Leah | cef0e6b625 | ||
Leah | edd16ecb24 | ||
Leah | b76f8c4af3 | ||
Leah | 05ff2feabf | ||
Leah | 94ed0d208d | ||
Leah | 1cf5c13e2b | ||
Leah | 6a9dd5b051 | ||
Leah | 599b1ef413 | ||
Leah | 7a20b78450 | ||
Leah | 5b9d62eac2 | ||
Leah | 4db20d159a | ||
Leah | 39f8df6ae9 | ||
Leah | 9f90eed214 | ||
Leah | 822996211f | ||
Leah | 81b67db9a1 | ||
Leah | e19f06a9d0 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -226,5 +226,3 @@ project.lock.json
|
|||
music/
|
||||
webmusic.linux.run
|
||||
dotnetwarp_temp
|
||||
|
||||
.bearer_token
|
||||
|
|
5
LICENSE
5
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Be Gay, Do Crimes License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Laura Hausmann
|
||||
|
||||
|
@ -9,9 +9,6 @@ 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:
|
||||
|
||||
Be Gay
|
||||
Do Crimes
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
@page
|
||||
@using System.IO
|
||||
@using System.Web
|
||||
@model IndexModel
|
||||
@{
|
||||
if (Model.LogConditionsMet)
|
||||
ViewData["Title"] = $"{Model.LogDisplay} ~ webmusic on .NET {Environment.Version}";
|
||||
else
|
||||
ViewData["Title"] = $"{Model.Displaypath} ~ webmusic on .NET {Environment.Version}";
|
||||
ViewData["Title"] = $"webmusic on .NET {Environment.Version}";
|
||||
}
|
||||
@if (Model.Path.Contains("/..")) {
|
||||
return;
|
||||
}
|
||||
@if (Model.Path.EndsWith(".lrc")) {
|
||||
Layout = "Shared/_LayoutNojs";
|
||||
<h3>@Model.Path</h3>
|
||||
<p>@Html.Raw((await System.IO.File.ReadAllTextAsync("music" + Model.Path)).Replace("\n", "<br/>"))</p>
|
||||
}
|
||||
|
@ -21,87 +16,34 @@ else {
|
|||
@Model.Displaypath <span id="state"></span> <span id="flags">[<span id="repeatButton">R</span><span id="continuousButton">C</span>]</span>
|
||||
</h2>
|
||||
<a class="action-muted">[..]</a>
|
||||
<a href="?@IndexModel.Encode(Model.PathOneup)" id="back" class="entry-muted cfont"> Go back</a>
|
||||
<a href="?@Model.PathOneup" id="back" class="entry-muted cfont"> Go back</a>
|
||||
<br/>
|
||||
<a class="action-muted">[--]</a>
|
||||
<a href="/playlist/@(Request.QueryString).m3u" class="entry-muted cfont"> Download playlist</a>
|
||||
@if (Model.LogConditionsMet) {
|
||||
<br/>
|
||||
<a class="action-muted">[--]</a>
|
||||
<span id="log" onclick="log_playback('/log?artist=@Model.LogArtist&album=@Model.LogAlbum&url=@Model.LogUrl')" class="entry-muted cfont" style="cursor: pointer">Log playback</span>
|
||||
<span class="action-muted">/</span>
|
||||
<span id="copy_hash_np" onclick="copy_hash_np()" class="entry-muted cfont" style="cursor: pointer">Copy #np</span>
|
||||
<span class="action-muted">/</span>
|
||||
<span id="copy_journal_full" onclick="copy_journal_full()" class="entry-muted cfont" style="cursor: pointer">Copy journal (full)</span>
|
||||
<span class="action-muted">/</span>
|
||||
<span id="copy_journal_album" onclick="copy_journal_album()" class="entry-muted cfont" style="cursor: pointer">Copy journal (album only)</span>
|
||||
}
|
||||
<br/>
|
||||
<br/>
|
||||
@foreach (var dir in Model.Dirs) {
|
||||
<li>
|
||||
<a class="action" href="#">[--]</a>
|
||||
<a class="dir" href="?@IndexModel.Encode(Model.Path + "/" + dir)"> @dir</a>
|
||||
<a class="action" href="#">[--]</a>
|
||||
<a class="dir" href="?@Model.Path/@dir"> @dir</a>
|
||||
</li>
|
||||
}
|
||||
@foreach (var file in Model.Files) {
|
||||
var jspath = IndexModel.Encode(Model.Fullpath);
|
||||
var jsfile = jspath + "/" + IndexModel.Encode(file);
|
||||
var basename = System.IO.Path.GetFileNameWithoutExtension(file);
|
||||
var lrcfile = basename + ".lrc";
|
||||
var lrcpath = System.IO.Path.Combine(Model.Fullpath, lrcfile);
|
||||
|
||||
<li>
|
||||
<a class="action" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))" download>[DL]</a>
|
||||
@if (System.IO.File.Exists(lrcpath)) {
|
||||
<a class="action" href="/lyrics/?@Html.Raw(IndexModel.Encode(Model.Path + "/" + lrcfile))" target="_blank">[LRC]</a>
|
||||
}
|
||||
else if (Directory.GetFiles(Model.Fullpath, "*.lrc").Length != 0) {
|
||||
<a class="action">[---]</a>
|
||||
}
|
||||
<a class="file" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))"> @file</a>
|
||||
<a class="action" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))" download>[DL]</a>
|
||||
@if (System.IO.File.Exists(lrcpath)) {
|
||||
<a class="action" href="?@Model.Path/@lrcfile" target="_blank">[LRC]</a>
|
||||
}
|
||||
else if(Directory.GetFiles(@Model.Fullpath, "*.lrc").Length != 0) {
|
||||
<a class="action">[---]</a>
|
||||
}
|
||||
<a class="file" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))"> @file</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<script>
|
||||
function log_playback(url) {
|
||||
fetch(url).then(function(response) {
|
||||
return response.text();
|
||||
}).then(function(text) {
|
||||
document.getElementById('log').innerText = 'Log playback (' + text + ')';
|
||||
});
|
||||
}
|
||||
|
||||
function reset_copy_labels() {
|
||||
document.getElementById('copy_hash_np').innerText = 'Copy #np';
|
||||
document.getElementById('copy_journal_full').innerText = 'Copy journal (full)';
|
||||
document.getElementById('copy_journal_album').innerText = 'Copy journal (album only)';
|
||||
}
|
||||
|
||||
function copy_hash_np() {
|
||||
copyToClipboard('[#np](@Html.Raw(Log.NowPlayingUrl)) @Html.Raw(Model.CopyArtist) - @Html.Raw(Model.CopyAlbum)');
|
||||
reset_copy_labels();
|
||||
document.getElementById('copy_hash_np').innerText = 'Copy #np (Copied!)';
|
||||
}
|
||||
|
||||
function copy_journal_full() {
|
||||
copyToClipboard('[@Html.Raw(Model.CopyArtist)](' + getUrl('@Html.Raw(Model.LogArtist)') + ') - [@Html.Raw(Model.CopyAlbum)](' + getUrl('@Html.Raw(Model.LogArtist)', '@Html.Raw(Model.LogAlbum)') + ')')
|
||||
reset_copy_labels();
|
||||
document.getElementById('copy_journal_full').innerText = 'Copy journal (full) (Copied!)';
|
||||
}
|
||||
|
||||
function copy_journal_album() {
|
||||
copyToClipboard(', [@Html.Raw(Model.CopyAlbum)](' + getUrl('@Html.Raw(Model.LogArtist)', '@Html.Raw(Model.LogAlbum)') + ')')
|
||||
reset_copy_labels();
|
||||
document.getElementById('copy_journal_album').innerText = 'Copy journal (album only) (Copied!)';
|
||||
}
|
||||
|
||||
function getUrl(artist, album) {
|
||||
let url = location.protocol + '//' + location.host + '/?/' + artist;
|
||||
|
||||
if (album) {
|
||||
url += '/' + album;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,25 +4,17 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace webmusic.Pages {
|
||||
public class IndexModel : PageModel {
|
||||
private const string Root = "music";
|
||||
public List<string> Dirs = new();
|
||||
public string Displaypath = "";
|
||||
public List<string> Files = new();
|
||||
public string Fullpath = "";
|
||||
public string Path = "";
|
||||
public string PathOneup = "";
|
||||
public bool LogConditionsMet;
|
||||
public string LogArtist;
|
||||
public string LogAlbum;
|
||||
public string LogDisplay;
|
||||
public string LogUrl;
|
||||
public string CopyArtist;
|
||||
public string CopyAlbum;
|
||||
private const string Root = "music";
|
||||
public List<string> Dirs = new();
|
||||
public string Displaypath = "";
|
||||
public List<string> Files = new();
|
||||
public string Fullpath = "";
|
||||
public string Path = "";
|
||||
public string PathOneup = "";
|
||||
|
||||
public void OnGet() {
|
||||
if (Request.QueryString.HasValue)
|
||||
|
@ -32,9 +24,9 @@ namespace webmusic.Pages {
|
|||
Response.Redirect("/Error");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (Path.EndsWith(".m3u"))
|
||||
Path = Path[..^4];
|
||||
Path = Path.Substring(0, Path.Length - 4);
|
||||
|
||||
if (Path.EndsWith(".lrc"))
|
||||
return;
|
||||
|
@ -49,79 +41,85 @@ namespace webmusic.Pages {
|
|||
Files.RemoveAll(p => p.EndsWith(".m3u"));
|
||||
Files.RemoveAll(p => p.EndsWith(".lrc"));
|
||||
Files.RemoveAll(p => p.StartsWith("."));
|
||||
Files.Sort(new NaturalSortComparer());
|
||||
if (Log.StatisticsEnabled && Request.Headers["Remote-User"].Equals(Log.StatisticsUser) && Files.Any()) {
|
||||
var pathparts = Displaypath.Split(System.IO.Path.DirectorySeparatorChar);
|
||||
if (pathparts.Length > 2) {
|
||||
CopyAlbum = pathparts[Index.FromEnd(1)].Replace("'", "\\'");
|
||||
CopyArtist = pathparts[Index.FromEnd(2)].Replace("'", "\\'");;
|
||||
LogAlbum = HttpUtility.UrlPathEncode(pathparts[Index.FromEnd(1)]).Replace("'", "\\'").Replace("&", "%26");
|
||||
LogArtist = HttpUtility.UrlPathEncode(pathparts[Index.FromEnd(2)]).Replace("'", "\\'").Replace("&", "%26");
|
||||
LogDisplay = $"{pathparts[Index.FromEnd(2)]} - {pathparts[Index.FromEnd(1)]}";
|
||||
LogUrl = Request.GetEncodedUrl().Replace("'", "\\'").Replace("&", "%26");
|
||||
LogConditionsMet = true;
|
||||
}
|
||||
}
|
||||
Files.Sort(new AlphanumComparatorFast());
|
||||
}
|
||||
|
||||
public static string Encode(string str) => str.Replace("\"", "%22").Replace("'", "%27").Replace("?", "%3F").Replace("&", "%26").Replace(" ", "%20".Replace("+", "%2B"));
|
||||
public static string Encode(string str) => str.Replace("\"", "%22")
|
||||
.Replace("'", "%27")
|
||||
.Replace("?", "%3F")
|
||||
.Replace("&", "%26")
|
||||
.Replace(" ", "%20");
|
||||
|
||||
private class NaturalSortComparer : IComparer<string>, IDisposable {
|
||||
private readonly bool _isAscending;
|
||||
|
||||
public NaturalSortComparer(bool inAscendingOrder = true) {
|
||||
_isAscending = inAscendingOrder;
|
||||
}
|
||||
|
||||
int IComparer<string>.Compare(string x, string y) {
|
||||
if (x == y)
|
||||
private class AlphanumComparatorFast : IComparer<string> {
|
||||
public int Compare(string x, string y) {
|
||||
var s1 = x;
|
||||
if (s1 == null)
|
||||
return 0;
|
||||
|
||||
if (!_table.TryGetValue(x!, out var x1)) {
|
||||
x1 = Regex.Split(x.Replace(" ", ""), "([0-9]+)");
|
||||
_table.Add(x, x1);
|
||||
}
|
||||
if (!(y is { } s2))
|
||||
return 0;
|
||||
|
||||
if (!_table.TryGetValue(y!, out var y1)) {
|
||||
y1 = Regex.Split(y.Replace(" ", ""), "([0-9]+)");
|
||||
_table.Add(y, y1);
|
||||
}
|
||||
var len1 = s1.Length;
|
||||
var len2 = s2.Length;
|
||||
var marker1 = 0;
|
||||
var marker2 = 0;
|
||||
|
||||
int returnVal;
|
||||
// Walk through two the strings with two markers.
|
||||
while (marker1 < len1 && marker2 < len2) {
|
||||
var ch1 = s1[marker1];
|
||||
var ch2 = s2[marker2];
|
||||
|
||||
for (var i = 0; i < x1.Length && i < y1.Length; i++) {
|
||||
if (x1[i] != y1[i]) {
|
||||
returnVal = PartCompare(x1[i], y1[i]);
|
||||
return _isAscending ? returnVal : -returnVal;
|
||||
// Some buffers we can build up characters in for each chunk.
|
||||
var space1 = new char[len1];
|
||||
var loc1 = 0;
|
||||
var space2 = new char[len2];
|
||||
var loc2 = 0;
|
||||
|
||||
// Walk through all following characters that are digits or
|
||||
// characters in BOTH strings starting at the appropriate marker.
|
||||
// Collect char arrays.
|
||||
do {
|
||||
space1[loc1++] = ch1;
|
||||
marker1++;
|
||||
|
||||
if (marker1 < len1)
|
||||
ch1 = s1[marker1];
|
||||
else
|
||||
break;
|
||||
} while (char.IsDigit(ch1) == char.IsDigit(space1[0]));
|
||||
|
||||
do {
|
||||
space2[loc2++] = ch2;
|
||||
marker2++;
|
||||
|
||||
if (marker2 < len2)
|
||||
ch2 = s2[marker2];
|
||||
else
|
||||
break;
|
||||
} while (char.IsDigit(ch2) == char.IsDigit(space2[0]));
|
||||
|
||||
// If we have collected numbers, compare them numerically.
|
||||
// Otherwise, if we have strings, compare them alphabetically.
|
||||
var str1 = new string(space1);
|
||||
var str2 = new string(space2);
|
||||
|
||||
int result;
|
||||
|
||||
if (char.IsDigit(space1[0]) && char.IsDigit(space2[0])) {
|
||||
var thisNumericChunk = int.Parse(str1);
|
||||
var thatNumericChunk = int.Parse(str2);
|
||||
result = thisNumericChunk.CompareTo(thatNumericChunk);
|
||||
}
|
||||
else {
|
||||
result = string.Compare(str1, str2, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (result != 0)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (y1.Length > x1.Length) {
|
||||
returnVal = 1;
|
||||
}
|
||||
else if (x1.Length > y1.Length) {
|
||||
returnVal = -1;
|
||||
}
|
||||
else {
|
||||
returnVal = 0;
|
||||
}
|
||||
|
||||
return _isAscending ? returnVal : -returnVal;
|
||||
}
|
||||
|
||||
private static int PartCompare(string left, string right) {
|
||||
if (!int.TryParse(left, out var x))
|
||||
return string.Compare(left, right, StringComparison.Ordinal);
|
||||
|
||||
return !int.TryParse(right, out var y) ? string.Compare(left, right, StringComparison.Ordinal) : x.CompareTo(y);
|
||||
}
|
||||
|
||||
private Dictionary<string, string[]> _table = new();
|
||||
|
||||
public void Dispose() {
|
||||
_table.Clear();
|
||||
_table = null;
|
||||
return len1 - len2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
Pages/Log.cs
54
Pages/Log.cs
|
@ -1,54 +0,0 @@
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace webmusic.Pages;
|
||||
|
||||
[ApiController, Route("/log")]
|
||||
public class Log : Controller {
|
||||
public const bool StatisticsEnabled = true;
|
||||
public const string StatisticsUser = "zotan";
|
||||
|
||||
public const string NowPlayingUrl = "https://zotan.pw/np";
|
||||
private const string StatisticsUrl = "https://zotan.pw/np/log";
|
||||
private const string StatisticsSource = "music.ztn.sh";
|
||||
private readonly string _statisticsToken = System.IO.File.Exists(".bearer_token") ? System.IO.File.ReadAllLines(".bearer_token")[0] : "";
|
||||
|
||||
[HttpGet]
|
||||
public string Get(string artist, string album, string url) {
|
||||
if (StatisticsEnabled && Request.Headers["Remote-User"].Equals(StatisticsUser)) {
|
||||
var res = MakeRestRequest(new LogPlaybackRequest { Artist = artist, Title = album, Link = url, Source = StatisticsSource });
|
||||
Response.StatusCode = (int)res;
|
||||
|
||||
return res switch {
|
||||
HttpStatusCode.Created => "Logged",
|
||||
HttpStatusCode.Accepted => "Skipped",
|
||||
HttpStatusCode.Forbidden => "Invalid token",
|
||||
_ => $"Error {Response.StatusCode} \"{res}\" occured"
|
||||
};
|
||||
}
|
||||
|
||||
Response.StatusCode = 403;
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpStatusCode MakeRestRequest(LogPlaybackRequest rq) {
|
||||
using var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(_statisticsToken);
|
||||
|
||||
var json = JsonSerializer.Serialize(rq);
|
||||
var data = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var response = client.PostAsync(StatisticsUrl, data).Result;
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
private class LogPlaybackRequest {
|
||||
public string Artist { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>@ViewData["Title"]</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet">
|
||||
<script src="/webmusic.js"></script>
|
||||
<link href="/webmusic.css" rel="stylesheet"/>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>@ViewData["Title"]</title>
|
||||
<link href="/webmusic.css" rel="stylesheet"/>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||
@page
|
||||
@using System.IO
|
||||
@model IndexModel
|
||||
@{
|
||||
ViewData["Title"] = $"{Model.Path.Split("/").Last()} ~ webmusic on .NET {Environment.Version}";
|
||||
}
|
||||
@if (Model.Path.Contains("/..")) {
|
||||
return;
|
||||
}
|
||||
@if (!Model.Path.EndsWith(".lrc")) {
|
||||
Response.Redirect("/" + Request.QueryString);
|
||||
return;
|
||||
}
|
||||
@{
|
||||
Layout = "Shared/_LayoutNojs";
|
||||
}
|
||||
<h3>@Model.Path</h3>
|
||||
<p>@Html.Raw((await System.IO.File.ReadAllTextAsync("music" + Model.Path)).Replace("\n", "<br/>"))</p>
|
|
@ -2,7 +2,7 @@
|
|||
@model IndexModel
|
||||
@{
|
||||
Layout = null;
|
||||
Response.ContentType = "text/plain; charset=utf-8";
|
||||
Response.ContentType = "text/plain";
|
||||
}
|
||||
@if (Model.Path.Contains("..")) {
|
||||
return;
|
||||
|
@ -12,4 +12,4 @@
|
|||
}
|
||||
@foreach (var file in Model.Files) {
|
||||
@Html.Raw(file + "\n")
|
||||
}
|
||||
}
|
|
@ -4,11 +4,11 @@
|
|||
@{
|
||||
var path = HttpUtility.UrlDecode(Request.QueryString.Value.TrimStart('?'));
|
||||
Layout = null;
|
||||
Response.ContentType = "text/plain; charset=utf-8";
|
||||
Response.ContentType = "text/plain";
|
||||
}
|
||||
@if (Model.Path.Contains("..")) {
|
||||
return;
|
||||
}
|
||||
@foreach (var file in Model.Files) {
|
||||
@Html.Raw("https://" + Request.Host + "/" + Model.Fullpath + "/" + file.Replace("?", "%3F") + "\n")
|
||||
}
|
||||
}
|
|
@ -1,12 +1,20 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:63053",
|
||||
"sslPort": 44395
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"webmusic": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5004",
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,2 @@
|
|||
## webmusic
|
||||
This project allows users to browse and play music in a minimal web interface.
|
||||
|
||||
Contains experimental support for collecting listening statistics in conjunction with [zotan.pw-web](https://git.ztn.sh/zotan/zotan.pw-web).
|
||||
This project allows users to browse and play music in a minimal web interface.
|
|
@ -23,6 +23,10 @@ namespace webmusic {
|
|||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
provider.Mappings[".flac"] = "audio/flac";
|
||||
provider.Mappings[".m3u"] = "audio/m3u";
|
||||
provider.Mappings[".opus"] = "audio/opus";
|
||||
if (env.IsDevelopment()) {
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
@ -32,11 +36,10 @@ namespace webmusic {
|
|||
app.UseHsts();
|
||||
}
|
||||
|
||||
var provider = new FileExtensionContentTypeProvider { Mappings = { [".flac"] = "audio/flac", [".m3u"] = "audio/m3u", [".opus"] = "audio/opus" } };
|
||||
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
|
||||
app.UseStaticFiles(new StaticFileOptions {ContentTypeProvider = provider});
|
||||
app.UseCookiePolicy();
|
||||
|
||||
app.UseMvc();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net70</TargetFramework>
|
||||
<TargetFramework>net50</TargetFramework>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -34,9 +34,4 @@
|
|||
<Content Remove="music\**" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="fonts" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
52
webmusic.css
52
webmusic.css
|
@ -1,23 +1,3 @@
|
|||
/* inconsolata-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''),
|
||||
url('../fonts/inconsolata-v21-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/inconsolata-v21-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* ubuntu-mono-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Ubuntu Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''),
|
||||
url('../fonts/ubuntu-mono-v10-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/ubuntu-mono-v10-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #161616;
|
||||
}
|
||||
|
@ -73,35 +53,3 @@ li {
|
|||
.selected {
|
||||
color: #8cd8cd;
|
||||
}
|
||||
|
||||
|
||||
/* Scrollbar colors */
|
||||
/* Works on Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: grey #161616;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #161616;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: grey;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
::-moz-selection { /* Code for Firefox */
|
||||
color: #161616;
|
||||
background: #d8c18c;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: #161616;
|
||||
background: #d8c18c;
|
||||
}
|
||||
|
|
368
webmusic.js
368
webmusic.js
|
@ -1,282 +1,262 @@
|
|||
'use strict';
|
||||
|
||||
const audioPlayer = new Audio();
|
||||
let selectedItem = 0;
|
||||
let playingItem = 0;
|
||||
let playerState = 'idle';
|
||||
let continuous = true;
|
||||
let repeat = false;
|
||||
let total = 0;
|
||||
let onlyDirs = true;
|
||||
const audioPlayer = new Audio();
|
||||
let selectedItem = 0;
|
||||
let playingItem = 0;
|
||||
let playerState = 'idle';
|
||||
let continuous = true;
|
||||
let repeat = false;
|
||||
let total = 0;
|
||||
let onlyDirs = true;
|
||||
|
||||
const handleKeyEvent = (event) => {
|
||||
if (event.ctrlKey === true || event.altKey === true || event.metaKey === true) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.ctrlKey === true || event.altKey === true) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
case 'p':
|
||||
if (onlyDirs !== false) return;
|
||||
if (playerState === 'idle' && total !== 0) {
|
||||
if (document.getElementById(playingItem).classList.contains('dir')) {
|
||||
return nextTrack();
|
||||
}
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
case 'p':
|
||||
if (onlyDirs !== false) return;
|
||||
if (playerState == 'idle' && total !== 0) {
|
||||
if (document.getElementById(playingItem).classList.contains('dir')) {
|
||||
return nextTrack();
|
||||
}
|
||||
|
||||
playSong(playingItem)
|
||||
} else {
|
||||
togglePlayback();
|
||||
}
|
||||
break;
|
||||
playSong(playingItem)
|
||||
} else {
|
||||
togglePlayback();
|
||||
}
|
||||
break;
|
||||
|
||||
case '>':
|
||||
nextTrack();
|
||||
break;
|
||||
case 'r':
|
||||
toggleRepeat();
|
||||
break;
|
||||
|
||||
case '<':
|
||||
previousTrack();
|
||||
break;
|
||||
case 'c':
|
||||
toggleContinue();
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
toggleRepeat();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
selectPreviousItem();
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
toggleContinue();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
selectNextItem();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
selectPreviousItem();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (audioPlayer.currentTime < 10) {
|
||||
audioPlayer.currentTime = 0;
|
||||
} else {
|
||||
audioPlayer.currentTime = audioPlayer.currentTime-10;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
selectNextItem();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
audioPlayer.currentTime = audioPlayer.currentTime+10;
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
if (audioPlayer.currentTime < 10) {
|
||||
audioPlayer.currentTime = 0;
|
||||
} else {
|
||||
audioPlayer.currentTime = audioPlayer.currentTime - 10;
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
document.getElementById(selectedItem).click()
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
audioPlayer.currentTime = audioPlayer.currentTime + 10;
|
||||
break;
|
||||
case 'Escape':
|
||||
document.getElementById('back').click();
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
if (document.activeElement.href) {
|
||||
document.activeElement.click();
|
||||
} else {
|
||||
document.getElementById(selectedItem).click();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
document.getElementById('back').click();
|
||||
break;
|
||||
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
audioPlayer.currentTime = audioPlayer.duration / 100 * (event.key * 10);
|
||||
break;
|
||||
}
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
audioPlayer.currentTime = audioPlayer.duration / 100 * (event.key * 10);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const initState = () => {
|
||||
const dirElements = document.querySelectorAll('.dir');
|
||||
const fileElements = document.querySelectorAll('.file');
|
||||
let id = 0;
|
||||
const dirElements = document.querySelectorAll('.dir');
|
||||
const fileElements = document.querySelectorAll('.file');
|
||||
let id = 0;
|
||||
|
||||
document.getElementById('state').addEventListener('click', togglePlayback)
|
||||
document.getElementById('repeatButton').addEventListener('click', toggleRepeat)
|
||||
document.getElementById('continuousButton').addEventListener('click', toggleContinue)
|
||||
document.getElementById('state').addEventListener('click', togglePlayback)
|
||||
document.getElementById('repeatButton').addEventListener('click', toggleRepeat)
|
||||
document.getElementById('continuousButton').addEventListener('click', toggleContinue)
|
||||
|
||||
audioPlayer.addEventListener('canplay', () => {
|
||||
audioPlayer.play();
|
||||
});
|
||||
audioPlayer.addEventListener('canplay', () => {
|
||||
audioPlayer.play();
|
||||
});
|
||||
|
||||
audioPlayer.addEventListener('play', () => {
|
||||
setPlayerState('playing');
|
||||
});
|
||||
audioPlayer.addEventListener('play', () => {
|
||||
setPlayerState('playing');
|
||||
});
|
||||
|
||||
audioPlayer.addEventListener('pause', () => {
|
||||
setPlayerState('paused');
|
||||
});
|
||||
audioPlayer.addEventListener('pause', () => {
|
||||
setPlayerState('paused');
|
||||
});
|
||||
|
||||
audioPlayer.addEventListener('error', () => {
|
||||
setPlayerState('error loading track');
|
||||
});
|
||||
audioPlayer.addEventListener('error', () => {
|
||||
setPlayerState('error loading track');
|
||||
});
|
||||
|
||||
audioPlayer.addEventListener('ended', () => {
|
||||
setPlayerState('idle');
|
||||
nextTrack();
|
||||
});
|
||||
audioPlayer.addEventListener('ended', () => {
|
||||
setPlayerState('idle');
|
||||
nextTrack();
|
||||
});
|
||||
|
||||
audioPlayer.addEventListener('timeupdate', () => {
|
||||
updatePlayerState();
|
||||
});
|
||||
audioPlayer.addEventListener('timeupdate', () => {
|
||||
updatePlayerState();
|
||||
});
|
||||
|
||||
dirElements.forEach((element) => {
|
||||
element.id = id++;
|
||||
});
|
||||
dirElements.forEach((element) => {
|
||||
element.id = id++;
|
||||
});
|
||||
|
||||
fileElements.forEach((element) => {
|
||||
element.id = id++;
|
||||
onlyDirs = false;
|
||||
fileElements.forEach((element) => {
|
||||
element.id = id++;
|
||||
onlyDirs = false;
|
||||
|
||||
element.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
playSong(event.target.id);
|
||||
});
|
||||
});
|
||||
element.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
playSong(event.target.id);
|
||||
});
|
||||
});
|
||||
|
||||
total = id;
|
||||
updatePlayerState();
|
||||
updateButtonState();
|
||||
total = id;
|
||||
updatePlayerState();
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
const setPlayerState = (state) => {
|
||||
playerState = state;
|
||||
playerState = state;
|
||||
|
||||
console.log('now in state: ' + state);
|
||||
updatePlayerState();
|
||||
console.log('now in state: ' + state);
|
||||
updatePlayerState();
|
||||
}
|
||||
|
||||
const updatePlayerState = () => {
|
||||
let statestr = '[' + playerState;
|
||||
let statestr = '[' + playerState;
|
||||
|
||||
if (!audioPlayer.paused) {
|
||||
statestr += ' ' + formatTime(audioPlayer.currentTime) + '/' + formatTime(audioPlayer.duration);
|
||||
}
|
||||
if (!audioPlayer.paused) {
|
||||
statestr += ' ' + formatTime(audioPlayer.currentTime) + '/' + formatTime(audioPlayer.duration);
|
||||
}
|
||||
|
||||
statestr += ']';
|
||||
document.getElementById('state').innerHTML = statestr;
|
||||
statestr += ']';
|
||||
document.getElementById('state').innerHTML = statestr;
|
||||
}
|
||||
|
||||
const updateButtonState = () => {
|
||||
if (repeat !== false) {
|
||||
document.getElementById('repeatButton').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('repeatButton').classList.remove('active');
|
||||
}
|
||||
if (repeat !== false) {
|
||||
document.getElementById('repeatButton').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('repeatButton').classList.remove('active');
|
||||
}
|
||||
|
||||
if (continuous !== false) {
|
||||
document.getElementById('continuousButton').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('continuousButton').classList.remove('active');
|
||||
}
|
||||
if (continuous !== false) {
|
||||
document.getElementById('continuousButton').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('continuousButton').classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
const playSong = (id) => {
|
||||
const element = document.getElementById(id);
|
||||
const element = document.getElementById(id);
|
||||
|
||||
if (element === null) return;
|
||||
if (element.classList.contains('dir')) return;
|
||||
if (element === null) return;
|
||||
if (element.classList.contains('dir')) return;
|
||||
|
||||
if (document.getElementsByClassName('playing').length > 0) {
|
||||
document.getElementsByClassName('playing')[0].classList.remove('playing');
|
||||
}
|
||||
if (document.getElementsByClassName('playing').length > 0) {
|
||||
document.getElementsByClassName('playing')[0].classList.remove('playing');
|
||||
}
|
||||
|
||||
if (document.getElementsByClassName('selected').length > 0) {
|
||||
document.getElementsByClassName('selected')[0].classList.remove('selected');
|
||||
}
|
||||
if (document.getElementsByClassName('selected').length > 0) {
|
||||
document.getElementsByClassName('selected')[0].classList.remove('selected');
|
||||
}
|
||||
|
||||
audioPlayer.pause()
|
||||
audioPlayer.pause()
|
||||
|
||||
playingItem = element.id;
|
||||
element.classList.add('playing');
|
||||
playingItem = element.id;
|
||||
element.classList.add('playing');
|
||||
|
||||
audioPlayer.src = element.href;
|
||||
audioPlayer.src = element.href;
|
||||
|
||||
setPlayerState('loading');
|
||||
audioPlayer.load();
|
||||
setPlayerState('loading');
|
||||
audioPlayer.load();
|
||||
}
|
||||
|
||||
const togglePlayback = () => {
|
||||
if (audioPlayer.paused) {
|
||||
audioPlayer.play();
|
||||
} else {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
if (audioPlayer.paused) {
|
||||
audioPlayer.play();
|
||||
} else {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRepeat = () => {
|
||||
repeat = !repeat;
|
||||
continuous = !repeat;
|
||||
audioPlayer.loop = repeat;
|
||||
updateButtonState();
|
||||
repeat = !repeat;
|
||||
continuous = !repeat;
|
||||
audioPlayer.loop = repeat;
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
const toggleContinue = () => {
|
||||
continuous = !continuous;
|
||||
updateButtonState();
|
||||
continuous = !continuous;
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
const previousTrack = () => {
|
||||
if (!continuous) return;
|
||||
if (playingItem-- === 0) playingItem = total - 1;
|
||||
if (!continuous) return;
|
||||
if (playingItem-- === 0) playingItem = total-1;
|
||||
|
||||
if (document.getElementById(playingItem).classList.contains('dir')) {
|
||||
return previousTrack();
|
||||
}
|
||||
if (document.getElementById(playingItem).classList.contains('dir')) {
|
||||
return previousTrack();
|
||||
}
|
||||
|
||||
playSong(playingItem);
|
||||
playSong(playingItem);
|
||||
}
|
||||
|
||||
const nextTrack = () => {
|
||||
if (!continuous) return;
|
||||
if (++playingItem === total) playingItem = 0;
|
||||
if (!continuous) return;
|
||||
if (++playingItem === total) playingItem = 0;
|
||||
|
||||
if (document.getElementById(playingItem).classList.contains('dir')) {
|
||||
return nextTrack();
|
||||
}
|
||||
if (document.getElementById(playingItem).classList.contains('dir')) {
|
||||
return nextTrack();
|
||||
}
|
||||
|
||||
playSong(playingItem);
|
||||
playSong(playingItem);
|
||||
}
|
||||
|
||||
const selectPreviousItem = () => {
|
||||
if (selectedItem-- === 0) selectedItem = total - 1;
|
||||
updateSelectedItem();
|
||||
if (selectedItem-- === 0) selectedItem = total-1;
|
||||
updateSelectedItem();
|
||||
}
|
||||
|
||||
const selectNextItem = () => {
|
||||
if (selectedItem === 0 && document.getElementsByClassName('selected').length === 0) {
|
||||
document.getElementById(selectedItem).classList.add('selected');
|
||||
} else {
|
||||
if (++selectedItem === total) selectedItem = 0;
|
||||
updateSelectedItem();
|
||||
}
|
||||
if (++selectedItem === total) selectedItem = 0;
|
||||
updateSelectedItem();
|
||||
}
|
||||
|
||||
const updateSelectedItem = () => {
|
||||
if (document.getElementsByClassName('selected').length > 0) {
|
||||
document.getElementsByClassName('selected')[0].classList.remove('selected');
|
||||
}
|
||||
if (document.getElementsByClassName('selected').length > 0) {
|
||||
document.getElementsByClassName('selected')[0].classList.remove('selected');
|
||||
}
|
||||
|
||||
document.getElementById(selectedItem).classList.add('selected');
|
||||
document.getElementById(selectedItem).classList.add('selected');
|
||||
}
|
||||
|
||||
const formatTime = (secs) => {
|
||||
secs = Math.round(secs);
|
||||
const minutes = Math.floor(secs / 60) || 0;
|
||||
const seconds = (secs - minutes * 60) || 0;
|
||||
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
|
||||
secs = Math.round(secs);
|
||||
const minutes = Math.floor(secs / 60) || 0;
|
||||
const seconds = (secs - minutes * 60) || 0;
|
||||
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initState);
|
||||
document.addEventListener('keydown', handleKeyEvent);
|
||||
|
||||
function copyToClipboard(str) {
|
||||
navigator.clipboard.writeText(str);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue