divan/GanjooRazor/Pages/SongRecommendation/MusicLink.cshtml
Ehsan Mohandesi cb7b1b03a3 Replace Spotify search with a validated music link box
The Spotify Web API integration stopped working, leaving /spotify unable to
suggest songs at all. Replace the artist/album/track search flow with a single
box where the user pastes a link to the track on a music service.

Links are checked against a hardcoded allow-list of legal streaming and store
domains so links to ripped audio cannot be submitted. The check lives in the
service layer, not just the page: any authenticated user can POST to
/api/ganjoor/song directly and bypass the UI. Hosts are matched against the
full host or a dot-prefixed suffix, so look-alikes such as
open.spotify.com.evil.com are rejected; https is required, and userinfo and
non-default ports are refused. Accepted URLs are canonicalised - https,
lowercased host, tracking parameters stripped - so the same track always yields
the same stored URL and duplicate detection actually works.

Links are stored under one new type, PoemMusicTrackType.MusicUrl, with the
platform derived from the host at render time. That needs no migration or
backfill, and supporting another service later needs no new enum value. The
duplicate check no longer keys on TrackType, which closes a gap where the same
URL could be resubmitted as a different type. Several links per poem remain
allowed; only an identical URL for the same poem is refused.

The Spotify search page and its OAuth plumbing are kept and simply redirect to
/musiclink while the existing SpotifyWorking flag is false, so the old flow can
be restored if that API ever works again.

Track URLs are no longer written through Html.Raw into href attributes. They
previously came from the Spotify API; now that they are user supplied, encoding
them prevents stored XSS.

Also fixes two latent bugs in SuggestSong that this flow would have hit: a null
dereference when TrackUrl is empty, and a singer lookup that matched any singer
with an empty Url.
2026-09-13 22:16:18 -05:00

156 lines
9.0 KiB
Plaintext

@page
@model GanjooRazor.Pages.MusicLinkModel
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="fa-IR" dir="rtl">
<head>
<meta charset="utf-8" />
<title>پیشنهاد پیوند قطعهٔ موسیقی مرتبط با شعر</title>
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="/favicon.ico?version=1" type="image/x-icon" />
<link href="/css/p8.css?version=89" rel="stylesheet" type="text/css" />
<link href="/css/spotify.css" rel="stylesheet" type="text/css" />
<script src="/lib/jquery/dist/jquery.min.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/musiclink.js?version=1"></script>
</head>
<body>
<div id="fa">
<div class="poem">
@if (Model.ReadOnlyMode)
{
<div class="width-100per">
<div class="width-100per text-align-center">
<p class="alert-text">
امکان پیشنهاد قطعه موقتا به دلیل تغییرات سایت و یا انتقال سرور وجود ندارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید.
</p>
</div>
</div>
}
else
@if (Model.PoemId == 0)
{
<div class="width-100per">
<div class="width-100per text-align-center">
<p class="alert-text">
خطا: شعری انتخاب نشده است.
</p>
</div>
</div>
}
else
if (!Model.LoggedIn)
{
<div class="width-100per">
<div class="width-100per text-align-center">
<p class="alert-text">
برای پیشنهاد آهنگهای مرتبط با اشعار لازم است ابتدا با نام کاربری خود وارد گنجور شوید.
</p>
<p class="text-align-center">
<a href=@Html.Raw($"/login/?redirect=/musiclink/?p={Model.PoemId}")>ورود به گنجور</a>
</p>
</div>
</div>
}
else
{
@if (!string.IsNullOrEmpty(Model.LastError))
{
<div class="width-100per">
<div class="width-100per text-align-center">
<p class="alert-text">
@Model.LastError
</p>
</div>
</div>
}
<div class="width-100per">
<div class="width-100per text-align-center">
<h3><strong>انتخاب منبع:</strong></h3>
</div>
<div style="width:100%;text-align:center">
<input type="radio" id="golha" name="source" value="گلها">
<label for="golha"><a href="@($"/golha/?p={Model.PoemId}")"><img src="/image/golha.png" alt="گلها" height="32" /></a></label>
<input type="radio" id="beeptunes" name="source" value="بیپ‌تونز">
<label for="beeptunes"><a href="@($"/bp/?p={Model.PoemId}")"><img src="/image/beeptunes.png" alt="بیپ‌تونز" height="32" /></a></label>
<input type="radio" id="musiclink" name="source" value="پیوند آهنگ" checked>
<label for="musiclink"><a href="@($"/musiclink/?p={Model.PoemId}")"><img src="/image/note.png" alt="پیوند آهنگ" height="32" /></a></label>
</div>
</div>
<p>
با استفاده از این صفحه می‌توانید آهنگهایی را که در متن آنها از شعر جاری استفاده شده است معرفی کنید. نشانی صفحهٔ آهنگ را از سرویس پخش یا فروش موسیقی کپی کرده و در جعبهٔ «نشانی آهنگ» بچسبانید. نام خواننده و عنوان آهنگ در صورت امکان به صورت خودکار پر می‌شود و می‌توانید آن را اصلاح کنید. پس از «پیشنهاد قطعه» آهنگ پیشنهادی در صف بررسی قرار می‌گیرد و پس از بازبینی و تأیید به صفحهٔ اصلی شعر اضافه می‌شود. از این که کمک می‌کنید که گنجور به نمایه‌ای برای موسیقی ایرانی بدل شود سپاسگزاریم.
</p>
<p>
<span style="color:red">تنها نشانی از این سرویس‌ها پذیرفته می‌شود:</span> @RMuseum.Utils.MusicUrlValidator.PlatformNames. نشانی فایلهای صوتی تکثیر شده و سایتهای دانلود غیرقانونی پذیرفته نمی‌شود.
</p>
<p>
<span style="color:red">تذکر مهم:</span> فقط مواردی تأیید می‌شود که <span style="color:red">در آهنگ متناظر همین شعر خوانده شود</span>. تعیین نام خواننده به تنهایی کارایی ندارد و لطفا جهت کاهش زمان مورد نیاز برای بررسی موارد پیشنهادی از اعلام موارد اینچنینی خودداری فرمایید. فهرست کامل آهنگهای مرتبط در <a href="https://ganjoor.net/mundex/" target="_blank">این صفحه</a> در دسترس است.
</p>
@if (Model.SuggestedSongs.Length > 0)
{
<p>موارد زیر شامل قطعه‌هایی هستند که پیشتر توسط دوستان دیگر پیشنهاد شده و در صف بررسی قرار دارند:</p>
@foreach (var suggestedSong in Model.SuggestedSongs)
{
<p style="@(suggestedSong.Id == Model.InsertedSongId ? "color:green" : "")">
@if (!string.IsNullOrEmpty(suggestedSong.ArtistName))
{
<text>@suggestedSong.ArtistName » </text>
}
@if (!string.IsNullOrEmpty(suggestedSong.AlbumName))
{
<text>@suggestedSong.AlbumName » </text>
}
<a target="_blank" rel="noopener nofollow" href="@suggestedSong.TrackUrl">@suggestedSong.TrackName</a>
</p>
}
}
@if (Model.PostSuccess)
{
<div class="width-100per">
<div class="width-100per text-align-center">
<p style="color:green">
با سپاس! پیشنهاد شما به فهرست اضافه شد.
</p>
</div>
</div>
}
}
</div>
@if (Model.PoemId != 0 && Model.LoggedIn && !Model.ReadOnlyMode)
{
<div class="content">
<form method="post">
<span class="inputlabel">نشانی آهنگ: </span>
<input type="url" dir="ltr" class="albumtrack" id="trackurl" placeholder="https://" required asp-for="PoemMusicTrackViewModel.TrackUrl" />
<div id="fetching-metadata" style="display:none"><img src="/image/loading.gif" alt="در حال دریافت مشخصات" /></div>
<br />
<span class="inputlabel">خواننده: </span>
<input type="text" class="albumtrack" placeholder="خواننده" required asp-for="PoemMusicTrackViewModel.ArtistName" />
<br />
<span class="inputlabel">آلبوم: </span>
<input type="text" class="albumtrack" placeholder="آلبوم (اختیاری)" asp-for="PoemMusicTrackViewModel.AlbumName" />
<br />
<span class="inputlabel">عنوان آهنگ: </span>
<input type="text" class="albumtrack" placeholder="عنوان آهنگ" required asp-for="PoemMusicTrackViewModel.TrackName" />
<br />
<span class="inputlabel">توضیح: </span>
<input type="text" class="albumtrack" placeholder="توضیح" asp-for="PoemMusicTrackViewModel.Description" />
<br />
<input type="submit" name="submit" id="submit" value="پیشنهاد قطعه" />
</form>
</div>
}
</div>
</body>
</html>