Compare commits
2 Commits
11d0b0d72f
...
ba9d97239e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba9d97239e | ||
|
|
8444fc5f8e |
23
PlaylistShared/Components/App.razor
Normal file
23
PlaylistShared/Components/App.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<ResourcePreloader />
|
||||
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["PlaylistShared.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
23
PlaylistShared/Components/Layout/MainLayout.razor
Normal file
23
PlaylistShared/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
98
PlaylistShared/Components/Layout/MainLayout.razor.css
Normal file
98
PlaylistShared/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,98 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
51
PlaylistShared/Components/Layout/NavMenu.razor
Normal file
51
PlaylistShared/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,51 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">PlaylistShared</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Главная
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="myplaylists">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Мои плейлисты
|
||||
</NavLink>
|
||||
</div>
|
||||
@if (user?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Logout">
|
||||
<span class="bi bi-box-arrow-right"></span> Выйти
|
||||
</NavLink>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Login">
|
||||
<span class="bi bi-box-arrow-in-right"></span> Войти
|
||||
</NavLink>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AuthenticationState? _authState;
|
||||
private ClaimsPrincipal? user => _authState?.User;
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthenticationStateTask { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthenticationStateTask is not null)
|
||||
_authState = await AuthenticationStateTask;
|
||||
}
|
||||
}
|
||||
108
PlaylistShared/Components/Layout/NavMenu.razor.css
Normal file
108
PlaylistShared/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,108 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
.bi-box-arrow-right, .bi-box-arrow-in-right {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z'/%3E%3Cpath fill-rule='evenodd' d='M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z'/%3E%3C/svg%3E");
|
||||
}
|
||||
31
PlaylistShared/Components/Layout/ReconnectModal.razor
Normal file
31
PlaylistShared/Components/Layout/ReconnectModal.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
|
||||
Resume
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
157
PlaylistShared/Components/Layout/ReconnectModal.razor.css
Normal file
157
PlaylistShared/Components/Layout/ReconnectModal.razor.css
Normal file
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
63
PlaylistShared/Components/Layout/ReconnectModal.razor.js
Normal file
63
PlaylistShared/Components/Layout/ReconnectModal.razor.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
80
PlaylistShared/Components/Pages/CreatePlaylist.razor
Normal file
80
PlaylistShared/Components/Pages/CreatePlaylist.razor
Normal file
@@ -0,0 +1,80 @@
|
||||
@page "/createplaylist"
|
||||
@attribute [Authorize]
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject AppDbContext Db
|
||||
@inject IYandexMusicService YandexService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<h3>Создание общего плейлиста</h3>
|
||||
|
||||
<EditForm Model="@model" OnValidSubmit="@HandleSubmit">
|
||||
<div class="mb-3">
|
||||
<label for="title">Название плейлиста</label>
|
||||
<InputText id="title" class="form-control" @bind-Value="model.Title" />
|
||||
<ValidationMessage For="@(() => model.Title)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description">Описание (необязательно)</label>
|
||||
<InputText id="description" class="form-control" @bind-Value="model.Description" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" disabled="@isLoading">@(isLoading ? "Создание..." : "Создать")</button>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
private CreateModel model = new();
|
||||
private bool isLoading;
|
||||
private string? userId;
|
||||
|
||||
public class CreateModel
|
||||
{
|
||||
[Required]
|
||||
public string Title { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
// пользователь не авторизован
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
// 1. Создаём плейлист в Яндекс Музыке
|
||||
var yandexId = await YandexService.CreatePlaylistAsync(userId, model.Title);
|
||||
// 2. Сохраняем в БД
|
||||
var playlist = new SharedPlaylist
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OwnerUserId = userId,
|
||||
YandexPlaylistId = yandexId,
|
||||
Title = model.Title,
|
||||
Description = model.Description,
|
||||
ShareSlug = Guid.NewGuid().ToString("N"),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
Db.SharedPlaylists.Add(playlist);
|
||||
await Db.SaveChangesAsync();
|
||||
Navigation.NavigateTo("/myplaylists");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// показать ошибку (можно добавить переменную errorMessage)
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
PlaylistShared/Components/Pages/Login.razor
Normal file
7
PlaylistShared/Components/Pages/Login.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/Login"
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
||||
<h3>Вход через Яндекс</h3>
|
||||
<a class="btn btn-primary" href="/signin-yandex">Войти через Яндекс</a>
|
||||
12
PlaylistShared/Components/Pages/Logout.razor
Normal file
12
PlaylistShared/Components/Pages/Logout.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
@page "/Logout"
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await SignInManager.SignOutAsync();
|
||||
Navigation.NavigateTo("/", true);
|
||||
}
|
||||
}
|
||||
57
PlaylistShared/Components/Pages/MyPlaylists.razor
Normal file
57
PlaylistShared/Components/Pages/MyPlaylists.razor
Normal file
@@ -0,0 +1,57 @@
|
||||
@page "/myplaylists"
|
||||
@attribute [Authorize]
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject AppDbContext Db
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<h3>Мои плейлисты</h3>
|
||||
<a href="/createplaylist" class="btn btn-success mb-3">Создать новый плейлист</a>
|
||||
|
||||
@if (playlists == null)
|
||||
{
|
||||
<p>Загрузка...</p>
|
||||
}
|
||||
else if (!playlists.Any())
|
||||
{
|
||||
<p>У вас пока нет общих плейлистов. Создайте первый!</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Название</th><th>Дата создания</th><th>Ссылка</th><th>Настройки</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var pl in playlists)
|
||||
{
|
||||
<tr>
|
||||
<td>@pl.Title</td>
|
||||
<td>@pl.CreatedAt.ToShortDateString()</td>
|
||||
<td><a href="/playlist/@pl.ShareSlug">открыть</a></td>
|
||||
<td><a href="/settings/@pl.Id">настройки</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<SharedPlaylist>? playlists;
|
||||
private string? userId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId != null)
|
||||
{
|
||||
playlists = await Db.SharedPlaylists
|
||||
.Where(p => p.OwnerUserId == userId)
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
PlaylistShared/Components/Pages/NotFound.razor
Normal file
5
PlaylistShared/Components/Pages/NotFound.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
92
PlaylistShared/Components/Pages/PlaylistDetails.razor
Normal file
92
PlaylistShared/Components/Pages/PlaylistDetails.razor
Normal file
@@ -0,0 +1,92 @@
|
||||
@page "/playlist/{slug}"
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using PlaylistShared.Data.Entities
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject AppDbContext Db
|
||||
@inject IYandexMusicService YandexService
|
||||
@inject IJSRuntime Js
|
||||
@inject HttpClient Http
|
||||
|
||||
<h3>@playlist?.Title</h3>
|
||||
<p>Владелец: @playlist?.Owner?.UserName</p>
|
||||
<p>Описание: @playlist?.Description</p>
|
||||
|
||||
@if (canAdd)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<input type="text" id="trackId" placeholder="ID трека Яндекс.Музыки" class="form-control" />
|
||||
<input type="text" id="trackTitle" placeholder="Название трека" class="form-control mt-1" />
|
||||
<input type="text" id="artistName" placeholder="Исполнитель" class="form-control mt-1" />
|
||||
<button class="btn btn-primary mt-2" id="addTrackBtn">Добавить</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ul id="trackList" class="list-group">
|
||||
@foreach (var track in tracks)
|
||||
{
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" data-track-id="@track.YandexTrackId">
|
||||
<div>
|
||||
<strong>@track.Title</strong> - @track.Artist
|
||||
</div>
|
||||
@if (canDelete(track))
|
||||
{
|
||||
<button class="btn btn-sm btn-danger deleteTrackBtn" data-track-id="@track.YandexTrackId">Удалить</button>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@code {
|
||||
[Parameter] public string slug { get; set; } = "";
|
||||
|
||||
private SharedPlaylist? playlist;
|
||||
private List<PlaylistTrack> tracks = new();
|
||||
private bool canAdd;
|
||||
private string? currentUserId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
currentUserId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
playlist = await Db.SharedPlaylists
|
||||
.Include(p => p.Owner)
|
||||
.Include(p => p.Tracks)
|
||||
.FirstOrDefaultAsync(p => p.ShareSlug == slug);
|
||||
if (playlist == null) return;
|
||||
|
||||
canAdd = playlist.Permissions.Add switch
|
||||
{
|
||||
AccessLevel.All => true,
|
||||
AccessLevel.Authorized => currentUserId != null,
|
||||
_ => false
|
||||
};
|
||||
|
||||
// Здесь можно при желании синхронизировать с Яндекс API, но пока используем локальный кеш
|
||||
// var yandexTracks = await YandexService.GetPlaylistTracksAsync(playlist.OwnerUserId, playlist.YandexPlaylistId);
|
||||
tracks = playlist.Tracks.OrderBy(t => t.AddedAt).ToList();
|
||||
}
|
||||
|
||||
private bool canDelete(PlaylistTrack track)
|
||||
{
|
||||
if (currentUserId == playlist?.OwnerUserId) return true;
|
||||
return playlist?.Permissions.Delete switch
|
||||
{
|
||||
DeleteAccessLevel.All => true,
|
||||
DeleteAccessLevel.Authorized => currentUserId != null,
|
||||
DeleteAccessLevel.AdderOnly => currentUserId != null && currentUserId == track.AddedByUserId,
|
||||
DeleteAccessLevel.OwnerOnly => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && playlist != null)
|
||||
{
|
||||
await Js.InvokeVoidAsync("initPlaylistInteractions", playlist.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
PlaylistShared/Components/Pages/PlaylistSettings.razor
Normal file
82
PlaylistShared/Components/Pages/PlaylistSettings.razor
Normal file
@@ -0,0 +1,82 @@
|
||||
@page "/settings/{id:guid}"
|
||||
@attribute [Authorize]
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using PlaylistShared.Data.Entities
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject AppDbContext Db
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<h3>Настройки плейлиста</h3>
|
||||
|
||||
@if (playlist == null)
|
||||
{
|
||||
<p>Загрузка...</p>
|
||||
}
|
||||
else if (!isOwner)
|
||||
{
|
||||
<p>Только владелец может изменять настройки.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm Model="@playlist.Permissions" OnValidSubmit="@SaveSettings">
|
||||
<div class="mb-3">
|
||||
<label>Кто может просматривать</label>
|
||||
<InputSelect @bind-Value="playlist.Permissions.View" class="form-control">
|
||||
<option value="@AccessLevel.All">Все</option>
|
||||
<option value="@AccessLevel.Authorized">Авторизованные</option>
|
||||
<option value="@AccessLevel.None">Никто</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Кто может добавлять треки</label>
|
||||
<InputSelect @bind-Value="playlist.Permissions.Add" class="form-control">
|
||||
<option value="@AccessLevel.All">Все</option>
|
||||
<option value="@AccessLevel.Authorized">Авторизованные</option>
|
||||
<option value="@AccessLevel.None">Никто</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Кто может удалять треки</label>
|
||||
<InputSelect @bind-Value="playlist.Permissions.Delete" class="form-control">
|
||||
<option value="@DeleteAccessLevel.All">Все</option>
|
||||
<option value="@DeleteAccessLevel.Authorized">Авторизованные</option>
|
||||
<option value="@DeleteAccessLevel.AdderOnly">Тот, кто добавил</option>
|
||||
<option value="@DeleteAccessLevel.OwnerOnly">Только владелец</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public Guid id { get; set; }
|
||||
private SharedPlaylist? playlist;
|
||||
private bool isOwner;
|
||||
private string? currentUserId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
currentUserId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
playlist = await Db.SharedPlaylists
|
||||
.Include(p => p.Owner)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (playlist != null)
|
||||
{
|
||||
isOwner = currentUserId == playlist.OwnerUserId;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
if (playlist != null)
|
||||
{
|
||||
Db.SharedPlaylists.Update(playlist);
|
||||
await Db.SaveChangesAsync();
|
||||
Navigation.NavigateTo("/myplaylists");
|
||||
}
|
||||
}
|
||||
}
|
||||
6
PlaylistShared/Components/Routes.razor
Normal file
6
PlaylistShared/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
11
PlaylistShared/Components/_Imports.razor
Normal file
11
PlaylistShared/Components/_Imports.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using PlaylistShared
|
||||
@using PlaylistShared.Components
|
||||
@using PlaylistShared.Components.Layout
|
||||
109
PlaylistShared/Controllers/AuthController.cs
Normal file
109
PlaylistShared/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlaylistShared.Models;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PlaylistShared.Controllers;
|
||||
|
||||
[Microsoft.AspNetCore.Mvc.Route("auth")]
|
||||
[ApiController]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly AppDbContext _db;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
|
||||
public AuthController(
|
||||
IConfiguration config,
|
||||
IHttpClientFactory factory,
|
||||
AppDbContext db,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
ILogger<AuthController> logger)
|
||||
{
|
||||
_config = config;
|
||||
_httpClient = factory.CreateClient();
|
||||
_db = db;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("login")]
|
||||
public IActionResult Login()
|
||||
{
|
||||
var clientId = _config["YandexOAuth:ClientId"];
|
||||
var redirectUri = Url.Action(nameof(Callback), "Auth", null, Request.Scheme);
|
||||
var authUrl = $"{_config["YandexOAuth:AuthorizationEndpoint"]}?response_type=code&client_id={clientId}&redirect_uri={Uri.EscapeDataString(redirectUri!)}";
|
||||
return Redirect(authUrl);
|
||||
}
|
||||
|
||||
[HttpGet("callback")]
|
||||
public async Task<IActionResult> Callback(string code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(code))
|
||||
return BadRequest("Missing code");
|
||||
|
||||
var tokenResponse = await ExchangeCodeForTokenAsync(code);
|
||||
if (tokenResponse is null)
|
||||
return StatusCode(500, "Token exchange failed");
|
||||
|
||||
var userInfo = await GetUserInfoAsync(tokenResponse.AccessToken);
|
||||
if (userInfo is null)
|
||||
return StatusCode(500, "User info fetch failed");
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.YandexId == userInfo.Id);
|
||||
if (user is null)
|
||||
{
|
||||
user = new ApplicationUser
|
||||
{
|
||||
UserName = userInfo.Login,
|
||||
YandexId = userInfo.Id,
|
||||
Email = userInfo.DefaultEmail,
|
||||
AccessToken = tokenResponse.AccessToken,
|
||||
RefreshToken = tokenResponse.RefreshToken,
|
||||
AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn)
|
||||
};
|
||||
_db.Users.Add(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.AccessToken = tokenResponse.AccessToken;
|
||||
user.RefreshToken = tokenResponse.RefreshToken;
|
||||
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
|
||||
_db.Users.Update(user);
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
private async Task<YandexTokenResponse?> ExchangeCodeForTokenAsync(string code)
|
||||
{
|
||||
var tokenUrl = _config["YandexOAuth:TokenEndpoint"];
|
||||
var redirectUri = Url.Action(nameof(Callback), "Auth", null, Request.Scheme);
|
||||
var content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("client_id", _config["YandexOAuth:ClientId"]!),
|
||||
new KeyValuePair<string, string>("client_secret", _config["YandexOAuth:ClientSecret"]!),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirectUri!)
|
||||
});
|
||||
|
||||
var response = await _httpClient.PostAsync(tokenUrl!, content);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<YandexTokenResponse>(json);
|
||||
}
|
||||
|
||||
private async Task<YandexUserInfo?> GetUserInfoAsync(string accessToken)
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", accessToken);
|
||||
var response = await _httpClient.GetAsync(_config["YandexOAuth:UserInfoEndpoint"]!);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<YandexUserInfo>(json);
|
||||
}
|
||||
}
|
||||
93
PlaylistShared/Controllers/TrackController.cs
Normal file
93
PlaylistShared/Controllers/TrackController.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
// Controllers/TrackController.cs
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace PlaylistShared.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Microsoft.AspNetCore.Mvc.Route("api/tracks")]
|
||||
[Authorize]
|
||||
public class TrackController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IYandexMusicService _yandex;
|
||||
|
||||
public TrackController(AppDbContext db, IYandexMusicService yandex)
|
||||
{
|
||||
_db = db;
|
||||
_yandex = yandex;
|
||||
}
|
||||
|
||||
[HttpPost("add")]
|
||||
public async Task<IActionResult> AddTrack(Guid playlistId, string trackId, string? trackTitle, string? artist)
|
||||
{
|
||||
var playlist = await _db.SharedPlaylists
|
||||
.Include(p => p.Owner)
|
||||
.FirstOrDefaultAsync(p => p.Id == playlistId);
|
||||
if (playlist == null) return NotFound();
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (!CanAdd(playlist, userId)) return Forbid();
|
||||
|
||||
// Добавляем в Яндекс
|
||||
await _yandex.AddTrackToPlaylistAsync(playlist.OwnerUserId, playlist.YandexPlaylistId, trackId);
|
||||
|
||||
// Сохраняем информацию о треке в кеш
|
||||
var track = new PlaylistTrack
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PlaylistId = playlist.Id,
|
||||
YandexTrackId = trackId,
|
||||
Title = trackTitle ?? "Unknown",
|
||||
Artist = artist,
|
||||
AddedByUserId = userId,
|
||||
AddedAt = DateTime.UtcNow
|
||||
};
|
||||
_db.PlaylistTracks.Add(track);
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("remove")]
|
||||
public async Task<IActionResult> RemoveTrack(Guid playlistId, string trackId)
|
||||
{
|
||||
var playlist = await _db.SharedPlaylists
|
||||
.Include(p => p.Owner)
|
||||
.FirstOrDefaultAsync(p => p.Id == playlistId);
|
||||
if (playlist == null) return NotFound();
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var trackEntry = await _db.PlaylistTracks
|
||||
.FirstOrDefaultAsync(t => t.PlaylistId == playlistId && t.YandexTrackId == trackId);
|
||||
if (!CanDelete(playlist, userId, trackEntry?.AddedByUserId))
|
||||
return Forbid();
|
||||
|
||||
await _yandex.RemoveTrackFromPlaylistAsync(playlist.OwnerUserId, playlist.YandexPlaylistId, trackId);
|
||||
if (trackEntry != null) _db.PlaylistTracks.Remove(trackEntry);
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private bool CanAdd(SharedPlaylist playlist, string? userId)
|
||||
{
|
||||
return playlist.Permissions.Add switch
|
||||
{
|
||||
AccessLevel.All => true,
|
||||
AccessLevel.Authorized => userId != null,
|
||||
AccessLevel.None => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private bool CanDelete(SharedPlaylist playlist, string? userId, string? adderUserId)
|
||||
{
|
||||
if (userId == playlist.OwnerUserId) return true; // владелец всегда может удалить
|
||||
return playlist.Permissions.Delete switch
|
||||
{
|
||||
DeleteAccessLevel.All => true,
|
||||
DeleteAccessLevel.Authorized => userId != null,
|
||||
DeleteAccessLevel.AdderOnly => userId != null && userId == adderUserId,
|
||||
DeleteAccessLevel.OwnerOnly => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
37
PlaylistShared/Data/Contexts/AppDbContext.cs
Normal file
37
PlaylistShared/Data/Contexts/AppDbContext.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PlaylistShared.Data.Entities;
|
||||
|
||||
namespace PlaylistShared.Data.Contexts;
|
||||
|
||||
public class AppDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
|
||||
public DbSet<PlaylistTrack> PlaylistTracks => Set<PlaylistTrack>();
|
||||
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.Entity<SharedPlaylist>(entity =>
|
||||
{
|
||||
entity.HasIndex(p => p.ShareSlug).IsUnique();
|
||||
entity.OwnsOne(p => p.Permissions, per =>
|
||||
{
|
||||
per.Property(x => x.View).HasColumnName("PermView");
|
||||
per.Property(x => x.Add).HasColumnName("PermAdd");
|
||||
per.Property(x => x.Delete).HasColumnName("PermDelete");
|
||||
});
|
||||
});
|
||||
|
||||
builder.Entity<PlaylistTrack>(entity =>
|
||||
{
|
||||
entity.HasOne(pt => pt.Playlist)
|
||||
.WithMany(p => p.Tracks)
|
||||
.HasForeignKey(pt => pt.PlaylistId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
10
PlaylistShared/Data/Entities/ApplicationUser.cs
Normal file
10
PlaylistShared/Data/Entities/ApplicationUser.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace PlaylistShared.Data.Entities;
|
||||
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
public string? YandexId { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public DateTime? AccessTokenExpiresAt { get; set; }
|
||||
public string? AvatarUrl { get; set; }
|
||||
}
|
||||
14
PlaylistShared/Data/Entities/PlaylistPermissions.cs
Normal file
14
PlaylistShared/Data/Entities/PlaylistPermissions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace PlaylistShared.Data.Entities;
|
||||
|
||||
public enum AccessLevel { All, Authorized, None }
|
||||
public enum DeleteAccessLevel { All, Authorized, AdderOnly, OwnerOnly }
|
||||
|
||||
[Owned]
|
||||
public class PlaylistPermissions
|
||||
{
|
||||
public AccessLevel View { get; set; } = AccessLevel.All;
|
||||
public AccessLevel Add { get; set; } = AccessLevel.Authorized;
|
||||
public DeleteAccessLevel Delete { get; set; } = DeleteAccessLevel.OwnerOnly;
|
||||
}
|
||||
23
PlaylistShared/Data/Entities/PlaylistTrack.cs
Normal file
23
PlaylistShared/Data/Entities/PlaylistTrack.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace PlaylistShared.Data.Entities;
|
||||
|
||||
public class PlaylistTrack
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid PlaylistId { get; set; }
|
||||
public SharedPlaylist Playlist { get; set; } = null!;
|
||||
|
||||
public string YandexTrackId { get; set; } = null!;
|
||||
public string Title { get; set; } = null!;
|
||||
public string? Artist { get; set; }
|
||||
public string? AlbumTitle { get; set; }
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
public string? AddedByUserId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(AddedByUserId))]
|
||||
public ApplicationUser? AddedByUser { get; set; }
|
||||
|
||||
public DateTime AddedAt { get; set; }
|
||||
}
|
||||
28
PlaylistShared/Data/Entities/SharedPlaylist.cs
Normal file
28
PlaylistShared/Data/Entities/SharedPlaylist.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace PlaylistShared.Data.Entities;
|
||||
|
||||
public class SharedPlaylist
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string OwnerUserId { get; set; } = null!;
|
||||
[ForeignKey(nameof(OwnerUserId))]
|
||||
public ApplicationUser Owner { get; set; } = null!;
|
||||
|
||||
public string YandexPlaylistId { get; set; } = null!;
|
||||
public string Title { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ShareSlug { get; set; } = null!;
|
||||
|
||||
public PlaylistPermissions Permissions { get; set; } = new();
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public List<PlaylistTrack> Tracks { get; set; } = new();
|
||||
}
|
||||
9
PlaylistShared/GlobalUsings.cs
Normal file
9
PlaylistShared/GlobalUsings.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
global using Microsoft.AspNetCore.Components;
|
||||
global using Microsoft.AspNetCore.Identity;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using PlaylistShared.Data.Contexts;
|
||||
global using PlaylistShared.Data.Entities;
|
||||
global using PlaylistShared.Services;
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
global using System.Security.Claims;
|
||||
4
PlaylistShared/Models/YandexOAuthModels.cs
Normal file
4
PlaylistShared/Models/YandexOAuthModels.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace PlaylistShared.Models;
|
||||
|
||||
public record YandexTokenResponse(string AccessToken, string RefreshToken, int ExpiresIn);
|
||||
public record YandexUserInfo(string Id, string Login, string? DisplayName, string? DefaultEmail, string? AvatarUrl);
|
||||
26
PlaylistShared/PlaylistShared.csproj
Normal file
26
PlaylistShared/PlaylistShared.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YandexMusic\YandexMusic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
79
PlaylistShared/Program.cs
Normal file
79
PlaylistShared/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PlaylistShared.Data.Contexts;
|
||||
using PlaylistShared.Data.Entities;
|
||||
using PlaylistShared.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddServerSideBlazor();
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));
|
||||
});
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<AppDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/Login";
|
||||
options.LogoutPath = "/Logout";
|
||||
options.AccessDeniedPath = "/AccessDenied";
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication()
|
||||
.AddOAuth("Yandex", options =>
|
||||
{
|
||||
options.ClientId = builder.Configuration["YandexOAuth:ClientId"];
|
||||
options.ClientSecret = builder.Configuration["YandexOAuth:ClientSecret"];
|
||||
options.AuthorizationEndpoint = builder.Configuration["YandexOAuth:AuthorizationEndpoint"];
|
||||
options.TokenEndpoint = builder.Configuration["YandexOAuth:TokenEndpoint"];
|
||||
options.UserInformationEndpoint = builder.Configuration["YandexOAuth:UserInfoEndpoint"];
|
||||
options.CallbackPath = "/signin-yandex";
|
||||
|
||||
options.ClaimActions.MapJsonKey("urn:yandex:avatar_url", "avatar_url");
|
||||
options.ClaimActions.MapJsonKey("urn:yandex:display_name", "display_name");
|
||||
|
||||
options.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents
|
||||
{
|
||||
OnCreatingTicket = async context =>
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
|
||||
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
|
||||
var response = await context.Backchannel.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var user = System.Text.Json.JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
context.RunClaimActions(user.RootElement);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<IYandexMusicService, YandexMusicService>();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapRazorPages();
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
app.Run();
|
||||
23
PlaylistShared/Properties/launchSettings.json
Normal file
23
PlaylistShared/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5058",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7272;http://localhost:5058",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
PlaylistShared/Services/IYandexMusicService.cs
Normal file
14
PlaylistShared/Services/IYandexMusicService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace PlaylistShared.Services;
|
||||
|
||||
public interface IYandexMusicService
|
||||
{
|
||||
Task<string> CreatePlaylistAsync(string userId, string title);
|
||||
Task AddTrackToPlaylistAsync(string userId, string yandexPlaylistId, string trackId);
|
||||
Task RemoveTrackFromPlaylistAsync(string userId, string yandexPlaylistId, string trackId);
|
||||
Task<List<YandexTrackInfo>> GetPlaylistTracksAsync(string userId, string yandexPlaylistId);
|
||||
Task<YandexPlaylistInfo> GetPlaylistInfoAsync(string userId, string yandexPlaylistId);
|
||||
Task<string?> RefreshUserTokenAsync(string userId);
|
||||
}
|
||||
|
||||
public record YandexTrackInfo(string Id, string Title, string? Artist, string? AlbumTitle, int DurationMs);
|
||||
public record YandexPlaylistInfo(string Kind, string Title, int TrackCount);
|
||||
173
PlaylistShared/Services/YandexMusicService.cs
Normal file
173
PlaylistShared/Services/YandexMusicService.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using PlaylistShared.Data.Contexts;
|
||||
using PlaylistShared.Data.Entities;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PlaylistShared.Services;
|
||||
|
||||
public class YandexMusicService : IYandexMusicService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<YandexMusicService> _logger;
|
||||
|
||||
public YandexMusicService(AppDbContext db, IHttpClientFactory factory, IConfiguration config, ILogger<YandexMusicService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_http = factory.CreateClient();
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<ApplicationUser> GetUserWithValidTokenAsync(string userId)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(userId);
|
||||
if (user == null) throw new Exception("User not found");
|
||||
|
||||
if (user.AccessTokenExpiresAt <= DateTime.UtcNow.AddMinutes(5))
|
||||
{
|
||||
var newToken = await RefreshYandexTokenAsync(user.RefreshToken!);
|
||||
user.AccessToken = newToken.access_token;
|
||||
user.RefreshToken = newToken.refresh_token;
|
||||
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(newToken.expires_in);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
private async Task<(string access_token, string refresh_token, int expires_in)> RefreshYandexTokenAsync(string refreshToken)
|
||||
{
|
||||
var content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", "refresh_token"),
|
||||
new KeyValuePair<string, string>("refresh_token", refreshToken),
|
||||
new KeyValuePair<string, string>("client_id", _config["YandexOAuth:ClientId"]),
|
||||
new KeyValuePair<string, string>("client_secret", _config["YandexOAuth:ClientSecret"])
|
||||
});
|
||||
var response = await _http.PostAsync(_config["YandexOAuth:TokenEndpoint"], content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return (
|
||||
doc.RootElement.GetProperty("access_token").GetString()!,
|
||||
doc.RootElement.GetProperty("refresh_token").GetString()!,
|
||||
doc.RootElement.GetProperty("expires_in").GetInt32()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<string> CreatePlaylistAsync(string userId, string title)
|
||||
{
|
||||
var user = await GetUserWithValidTokenAsync(userId);
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
|
||||
var payload = new { title, visibility = "public" };
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var response = await _http.PostAsync("https://api.music.yandex.net/users/self/playlists/create",
|
||||
new StringContent(json, Encoding.UTF8, "application/json"));
|
||||
response.EnsureSuccessStatusCode();
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistData>>(resultJson);
|
||||
return result!.Result.Kind;
|
||||
}
|
||||
|
||||
public async Task AddTrackToPlaylistAsync(string userId, string yandexPlaylistId, string trackId)
|
||||
{
|
||||
var user = await GetUserWithValidTokenAsync(userId);
|
||||
var uid = user.YandexId; // или user.Id? В API Яндекса используется uid из аккаунта, но можно использовать "self"
|
||||
// Получаем альбом трека (опционально, можно передать без albumId)
|
||||
// Для простоты используем трек без albumId
|
||||
var diff = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
op = "insert",
|
||||
at = 0,
|
||||
tracks = new[] { new { id = trackId } }
|
||||
}
|
||||
};
|
||||
var json = JsonSerializer.Serialize(diff);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}/change";
|
||||
var response = await _http.PostAsync(url, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task RemoveTrackFromPlaylistAsync(string userId, string yandexPlaylistId, string trackId)
|
||||
{
|
||||
var user = await GetUserWithValidTokenAsync(userId);
|
||||
var uid = user.YandexId;
|
||||
// Сначала нужно получить позицию трека в плейлисте – упростим: удаляем по индексу (не надёжно)
|
||||
// Лучше получить плейлист, найти индекс трека, потом удалить
|
||||
var playlist = await GetPlaylistInfoAsync(userId, yandexPlaylistId);
|
||||
var tracks = await GetPlaylistTracksAsync(userId, yandexPlaylistId);
|
||||
var index = tracks.FindIndex(t => t.Id == trackId);
|
||||
if (index == -1) throw new Exception("Track not found in playlist");
|
||||
var diff = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
op = "delete",
|
||||
from = index,
|
||||
to = index + 1
|
||||
}
|
||||
};
|
||||
var json = JsonSerializer.Serialize(diff);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}/change";
|
||||
var response = await _http.PostAsync(url, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<List<YandexTrackInfo>> GetPlaylistTracksAsync(string userId, string yandexPlaylistId)
|
||||
{
|
||||
var user = await GetUserWithValidTokenAsync(userId);
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
|
||||
var uid = user.YandexId;
|
||||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}";
|
||||
var response = await _http.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var data = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistFull>>(json);
|
||||
return data!.Result.Tracks?.Select(t => new YandexTrackInfo(
|
||||
t.Id.ToString(),
|
||||
t.Title,
|
||||
t.Artists?.FirstOrDefault()?.Name,
|
||||
t.Albums?.FirstOrDefault()?.Title,
|
||||
t.DurationMs
|
||||
)).ToList() ?? new();
|
||||
}
|
||||
|
||||
public async Task<YandexPlaylistInfo> GetPlaylistInfoAsync(string userId, string yandexPlaylistId)
|
||||
{
|
||||
var user = await GetUserWithValidTokenAsync(userId);
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
|
||||
var uid = user.YandexId;
|
||||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}";
|
||||
var response = await _http.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var data = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistFull>>(json);
|
||||
return new YandexPlaylistInfo(data!.Result.Kind, data.Result.Title, data.Result.TrackCount);
|
||||
}
|
||||
|
||||
public async Task<string?> RefreshUserTokenAsync(string userId)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(userId);
|
||||
if (user?.RefreshToken == null) return null;
|
||||
var newToken = await RefreshYandexTokenAsync(user.RefreshToken);
|
||||
user.AccessToken = newToken.access_token;
|
||||
user.RefreshToken = newToken.refresh_token;
|
||||
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(newToken.expires_in);
|
||||
await _db.SaveChangesAsync();
|
||||
return newToken.access_token;
|
||||
}
|
||||
|
||||
// Вспомогательные record
|
||||
private record YandexApiResponse<T>(T Result);
|
||||
private record YandexPlaylistData(string Kind);
|
||||
private record YandexPlaylistFull(string Kind, string Title, int TrackCount, List<YandexTrack> Tracks);
|
||||
private record YandexTrack(string Id, string Title, int DurationMs, List<YandexArtist> Artists, List<YandexAlbum> Albums);
|
||||
private record YandexArtist(string Name);
|
||||
private record YandexAlbum(string Title);
|
||||
}
|
||||
8
PlaylistShared/appsettings.Development.json
Normal file
8
PlaylistShared/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
PlaylistShared/appsettings.json
Normal file
24
PlaylistShared/appsettings.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"YandexOAuth": {
|
||||
"ClientId": "0916685f8a3641ca8fc382dbccf77236",
|
||||
"ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698",
|
||||
"AuthorizationEndpoint": "https://oauth.yandex.ru/authorize",
|
||||
"TokenEndpoint": "https://oauth.yandex.ru/token",
|
||||
"UserInfoEndpoint": "https://login.yandex.ru/info"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "very_long_secret_key_at_least_32_chars",
|
||||
"Issuer": "PlaylistShared",
|
||||
"Audience": "PlaylistSharedAPI"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data Source=playlist.db"
|
||||
}
|
||||
}
|
||||
60
PlaylistShared/wwwroot/app.css
Normal file
60
PlaylistShared/wwwroot/app.css
Normal file
@@ -0,0 +1,60 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
BIN
PlaylistShared/wwwroot/favicon.png
Normal file
BIN
PlaylistShared/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
26
PlaylistShared/wwwroot/js/site.js
Normal file
26
PlaylistShared/wwwroot/js/site.js
Normal file
@@ -0,0 +1,26 @@
|
||||
window.initPlaylistInteractions = (playlistId) => {
|
||||
const addBtn = document.getElementById('addTrackBtn');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', async () => {
|
||||
const trackId = document.getElementById('trackId').value;
|
||||
const trackTitle = document.getElementById('trackTitle').value;
|
||||
const artist = document.getElementById('artistName').value;
|
||||
const response = await fetch(`/api/tracks/add?playlistId=${playlistId}&trackId=${trackId}&trackTitle=${encodeURIComponent(trackTitle)}&artist=${encodeURIComponent(artist)}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (response.ok) location.reload();
|
||||
else alert('Ошибка добавления трека');
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.deleteTrackBtn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const trackId = btn.getAttribute('data-track-id');
|
||||
const response = await fetch(`/api/tracks/remove?playlistId=${playlistId}&trackId=${trackId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (response.ok) location.reload();
|
||||
else alert('Ошибка удаления трека');
|
||||
});
|
||||
});
|
||||
};
|
||||
4085
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4085
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4084
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4084
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
597
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
597
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
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: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
594
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
594
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
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: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5402
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
5402
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5393
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
5393
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12057
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
12057
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12030
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
12030
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
6
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6314
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
6314
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4447
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
4447
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
7
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4494
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
4494
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
vendored
Normal file
7
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
vendored
Normal file
1
PlaylistShared/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,34 +5,24 @@ using YandexMusic.API.Requests.Album;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// API для взаимодействия с альбомами
|
||||
/// </summary>
|
||||
/// <summary>API для работы с альбомами.</summary>
|
||||
public class YAlbumAPI : YCommonAPI
|
||||
{
|
||||
public YAlbumAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
/// <summary>Инициализирует новый экземпляр API альбомов.</summary>
|
||||
/// <param name="yandex">Экземпляр основного API.</param>
|
||||
public YAlbumAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||
|
||||
/// <summary>Получает альбом по идентификатору.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <param name="storage">Хранилище данных авторизации.</param>
|
||||
/// <param name="albumId">Идентификатор альбома.</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>Ответ API с моделью альбома.</returns>
|
||||
public Task<YResponse<YAlbum>> GetAsync(AuthStorage storage, string albumId)
|
||||
{
|
||||
return new YGetAlbumBuilder(api, storage)
|
||||
.Build(albumId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
=> new YGetAlbumBuilder(api, storage).Build(albumId).GetResponseAsync();
|
||||
|
||||
/// <summary>Получение альбомов по списку идентификаторов.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <param name="albumIds">Идентификаторы альбомов.</param>
|
||||
/// <returns></returns>
|
||||
/// <summary>Получает несколько альбомов по списку идентификаторов.</summary>
|
||||
/// <param name="storage">Хранилище данных авторизации.</param>
|
||||
/// <param name="albumIds">Список идентификаторов альбомов.</param>
|
||||
/// <returns>Ответ API со списком альбомов.</returns>
|
||||
public Task<YResponse<List<YAlbum>>> GetAsync(AuthStorage storage, IEnumerable<string> albumIds)
|
||||
{
|
||||
return new YGetAlbumsBuilder(api, storage)
|
||||
.Build(albumIds)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
=> new YGetAlbumsBuilder(api, storage).Build(albumIds).GetResponseAsync();
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// Родительский класс для ветки API
|
||||
/// </summary>
|
||||
public class YCommonAPI
|
||||
/// <summary>Родительский класс для всех веток API.</summary>
|
||||
public abstract class YCommonAPI
|
||||
{
|
||||
protected YandexMusicApi api;
|
||||
/// <summary>Основной экземпляр API.</summary>
|
||||
protected readonly YandexMusicApi api;
|
||||
|
||||
public YCommonAPI(YandexMusicApi yandex)
|
||||
{
|
||||
api = yandex;
|
||||
}
|
||||
/// <summary>Инициализирует новый экземпляр.</summary>
|
||||
/// <param name="yandex">Экземпляр основного API.</param>
|
||||
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
|
||||
}
|
||||
39
YandexMusic.API/API/YLabelAPI.cs
Normal file
39
YandexMusic.API/API/YLabelAPI.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Label;
|
||||
using YandexMusic.API.Requests.Label;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
public partial class YLabelAPI : YCommonAPI
|
||||
{
|
||||
public YLabelAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Постраничное получение альбомов лейбла
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="label">Лейбл</param>
|
||||
/// <param name="page">Страница</param>
|
||||
public Task<YResponse<YLabelAlbums>> GetAlbumsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
||||
{
|
||||
return new YGetLabelAlbumsBuilder(api, storage)
|
||||
.Build((label, page))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Постраничное получение артистов лейбла
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="label">Лейбл</param>
|
||||
/// <param name="page">Страница</param>
|
||||
public Task<YResponse<YLabelArtists>> GetArtistsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
||||
{
|
||||
return new YGetLabelArtistsBuilder(api, storage)
|
||||
.Build((label, page))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Label;
|
||||
using YandexMusic.API.Requests.Label;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
public partial class YLabelAPI : YCommonAPI
|
||||
{
|
||||
public YLabelAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Постраничное получение альбомов лейбла
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="label">Лейбл</param>
|
||||
/// <param name="page">Страница</param>
|
||||
public Task<YResponse<YLabelAlbums>> GetAlbumsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
||||
{
|
||||
return new YGetLabelAlbumsBuilder(api, storage)
|
||||
.Build((label, page))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Постраничное получение артистов лейбла
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="label">Лейбл</param>
|
||||
/// <param name="page">Страница</param>
|
||||
public Task<YResponse<YLabelArtists>> GetArtistsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
||||
{
|
||||
return new YGetLabelArtistsBuilder(api, storage)
|
||||
.Build((label, page))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
YandexMusic.API/API/YLandingAPI.cs
Normal file
43
YandexMusic.API/API/YLandingAPI.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Feed;
|
||||
using YandexMusic.API.Models.Landing;
|
||||
using YandexMusic.API.Requests.Feed;
|
||||
using YandexMusic.API.Requests.Landing;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>API для взаимодействия с главной страницей (лендингом).</summary>
|
||||
public class YLandingAPI : YCommonAPI
|
||||
{
|
||||
/// <summary>Инициализирует новый экземпляр API лендинга.</summary>
|
||||
/// <param name="yandex">Экземпляр основного API.</param>
|
||||
public YLandingAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||
|
||||
/// <summary>Получает персональные блоки лендинга.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <param name="blocks">Типы запрашиваемых блоков.</param>
|
||||
/// <returns>Ответ API с лендингом.</returns>
|
||||
/// <exception cref="ArgumentNullException">Если массив blocks равен null.</exception>
|
||||
public Task<YResponse<YLanding>> GetAsync(AuthStorage storage, params YLandingBlockType[] blocks)
|
||||
{
|
||||
if (blocks == null)
|
||||
throw new ArgumentNullException(nameof(blocks), "Массив блоков не может быть null");
|
||||
|
||||
return new YGetLandingBuilder(api, storage)
|
||||
.Build(blocks)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>Получает ленту событий (фид).</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <returns>Ответ API с лентой.</returns>
|
||||
public Task<YResponse<YFeed>> GetFeedAsync(AuthStorage storage)
|
||||
=> new YGetFeedBuilder(api, storage).Build(null!).GetResponseAsync();
|
||||
|
||||
/// <summary>Получает лендинг детского раздела.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <returns>Ответ API с детским лендингом.</returns>
|
||||
public Task<YResponse<YChildrenLanding>> GetChildrenLandingAsync(AuthStorage storage)
|
||||
=> new YGetChildrenLandingBuilder(api, storage).Build(null!).GetResponseAsync();
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Feed;
|
||||
using YandexMusic.API.Models.Landing;
|
||||
using YandexMusic.API.Requests.Feed;
|
||||
using YandexMusic.API.Requests.Landing;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взаимодействия с главной страницей
|
||||
/// </summary>
|
||||
public partial class YLandingAPI : YCommonAPI
|
||||
{
|
||||
|
||||
|
||||
public YLandingAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение персональных списков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="blocks">Типы запрашиваемых блоков</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YLanding>> GetAsync(AuthStorage storage, params YLandingBlockType[] blocks)
|
||||
{
|
||||
if (blocks == null)
|
||||
return null;
|
||||
|
||||
return new YGetLandingBuilder(api, storage)
|
||||
.Build(blocks)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение ленты
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YFeed>> GetFeedAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetFeedBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лендинга детского раздела
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YChildrenLanding>> GetChildrenLandingAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetChildrenLandingBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
253
YandexMusic.API/API/YLibraryAPI.cs
Normal file
253
YandexMusic.API/API/YLibraryAPI.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Album;
|
||||
using YandexMusic.API.Models.Artist;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
|
||||
using YandexMusic.API.Models.Library;
|
||||
using YandexMusic.API.Models.Playlist;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Library;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// API для взаимодействия с библиотекой
|
||||
/// </summary>
|
||||
public partial class YLibraryAPI : YCommonAPI
|
||||
{
|
||||
/// <summary>
|
||||
/// Получение секции библиотеки
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип объекта библиотеки</typeparam>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="section">Секция</param>
|
||||
/// <param name="type">Тип</param>
|
||||
/// <returns>Список объектов из секции</returns>
|
||||
private Task<YResponse<T>> GetLibrarySection<T>(AuthStorage storage, YLibrarySection section, YLibrarySectionType type = YLibrarySectionType.Likes)
|
||||
{
|
||||
return new YGetLibrarySectionBuilder<T>(api, storage)
|
||||
.Build((section, type))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public YLibraryAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
#region Лайки
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YLibraryTracks>> GetLikedTracksAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых альбомов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YLibraryAlbum>>> GetLikedAlbumsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YLibraryAlbum>>(storage, YLibrarySection.Albums);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых исполнителей
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YArtist>>> GetLikedArtistsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых плейлистов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YLibraryPlaylists>>> GetLikedPlaylistsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YLibraryPlaylists>>(storage, YLibrarySection.Playlists);
|
||||
}
|
||||
|
||||
#endregion Лайки
|
||||
|
||||
#region Дизлайки
|
||||
|
||||
/// <summary>
|
||||
/// Получение дизлайкнутых треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YLibraryTracks>> GetDislikedTracksAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks, YLibrarySectionType.Dislikes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение дизлайкнутых исполнителей
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YArtist>>> GetDislikedArtistsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists, YLibrarySectionType.Dislikes);
|
||||
}
|
||||
|
||||
#endregion Дизлайки
|
||||
|
||||
#region Добавление в списки лайков/дизлайков
|
||||
|
||||
/// <summary>
|
||||
/// Добавить трек в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> AddTrackLikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryAddBuilder<YPlaylist>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить трек из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YRevision>> RemoveTrackLikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить трек в список дизлайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YRevision>> AddTrackDislikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryAddBuilder<YRevision>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить трек из списка дизлайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YRevision>> RemoveTrackDislikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить альбом в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="album">Альбом</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> AddAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
||||
{
|
||||
return new YLibraryAddBuilder<string>(api, storage)
|
||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить альбом из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="album">Альбом</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> RemoveAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить исполнителя в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="artist">Исполнитель</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> AddArtistLikeAsync(AuthStorage storage, YArtist artist)
|
||||
{
|
||||
return new YLibraryAddBuilder<string>(api, storage)
|
||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить исполнителя из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="artist">Исполнитель</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> RemoveArtistLikeAsync(AuthStorage storage, YArtist artist)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить плейлист в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> AddPlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
||||
{
|
||||
return new YLibraryAddBuilder<string>(api, storage)
|
||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить плейлист из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> RemovePlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion Добавление/удаление в списки лайков/дизлайков
|
||||
|
||||
#region Получение списка "Вы недавно слушали"
|
||||
|
||||
public Task<YResponse<YRecentlyListenedContext>> GetRecentlyListenedAsync(AuthStorage storage, IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)
|
||||
{
|
||||
return new YGetLibraryRecentlyListenedBuilder(api, storage)
|
||||
.Build((contextTypes, trackCount, contextCount))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion Получение списка "Вы недавно слушали"
|
||||
|
||||
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Album;
|
||||
using YandexMusic.API.Models.Artist;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
|
||||
using YandexMusic.API.Models.Library;
|
||||
using YandexMusic.API.Models.Playlist;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Library;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взаимодействия с библиотекой
|
||||
/// </summary>
|
||||
public partial class YLibraryAPI : YCommonAPI
|
||||
{
|
||||
#region Вспомогательные функции
|
||||
|
||||
/// <summary>
|
||||
/// Получение секции библиотеки
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип объекта библиотеки</typeparam>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="section">Секция</param>
|
||||
/// <param name="type">Тип</param>
|
||||
/// <returns>Список объектов из секции</returns>
|
||||
private Task<YResponse<T>> GetLibrarySection<T>(AuthStorage storage, YLibrarySection section, YLibrarySectionType type = YLibrarySectionType.Likes)
|
||||
{
|
||||
return new YGetLibrarySectionBuilder<T>(api, storage)
|
||||
.Build((section, type))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public YLibraryAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
#region Лайки
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YLibraryTracks>> GetLikedTracksAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых альбомов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YLibraryAlbum>>> GetLikedAlbumsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YLibraryAlbum>>(storage, YLibrarySection.Albums);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых исполнителей
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YArtist>>> GetLikedArtistsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение лайкнутых плейлистов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YLibraryPlaylists>>> GetLikedPlaylistsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YLibraryPlaylists>>(storage, YLibrarySection.Playlists);
|
||||
}
|
||||
|
||||
#endregion Лайки
|
||||
|
||||
#region Дизлайки
|
||||
|
||||
/// <summary>
|
||||
/// Получение дизлайкнутых треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YLibraryTracks>> GetDislikedTracksAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks, YLibrarySectionType.Dislikes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение дизлайкнутых исполнителей
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YArtist>>> GetDislikedArtistsAsync(AuthStorage storage)
|
||||
{
|
||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists, YLibrarySectionType.Dislikes);
|
||||
}
|
||||
|
||||
#endregion Дизлайки
|
||||
|
||||
#region Добавление в списки лайков/дизлайков
|
||||
|
||||
/// <summary>
|
||||
/// Добавить трек в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> AddTrackLikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryAddBuilder<YPlaylist>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить трек из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YRevision>> RemoveTrackLikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить трек в список дизлайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YRevision>> AddTrackDislikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryAddBuilder<YRevision>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить трек из списка дизлайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YRevision>> RemoveTrackDislikeAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить альбом в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="album">Альбом</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> AddAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
||||
{
|
||||
return new YLibraryAddBuilder<string>(api, storage)
|
||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить альбом из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="album">Альбом</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> RemoveAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить исполнителя в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="artist">Исполнитель</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> AddArtistLikeAsync(AuthStorage storage, YArtist artist)
|
||||
{
|
||||
return new YLibraryAddBuilder<string>(api, storage)
|
||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить исполнителя из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="artist">Исполнитель</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> RemoveArtistLikeAsync(AuthStorage storage, YArtist artist)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить плейлист в список лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> AddPlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
||||
{
|
||||
return new YLibraryAddBuilder<string>(api, storage)
|
||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить плейлист из списка лайкнутых
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> RemovePlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
||||
{
|
||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion Добавление/удаление в списки лайков/дизлайков
|
||||
|
||||
#region Получение списка "Вы недавно слушали"
|
||||
|
||||
public Task<YResponse<YRecentlyListenedContext>> GetRecentlyListenedAsync(AuthStorage storage, IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)
|
||||
{
|
||||
return new YGetLibraryRecentlyListenedBuilder(api, storage)
|
||||
.Build((contextTypes, trackCount, contextCount))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion Получение списка "Вы недавно слушали"
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
20
YandexMusic.API/API/YPinsAPI.cs
Normal file
20
YandexMusic.API/API/YPinsAPI.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Pins;
|
||||
using YandexMusic.API.Requests.Pins;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>API для взаимодействия с закреплёнными объектами (пинами).</summary>
|
||||
public class YPinsAPI : YCommonAPI
|
||||
{
|
||||
/// <summary>Инициализирует новый экземпляр API пинов.</summary>
|
||||
/// <param name="yandex">Экземпляр основного API.</param>
|
||||
public YPinsAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||
|
||||
/// <summary>Получает список закреплённых объектов.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <returns>Ответ API со списком пинов.</returns>
|
||||
public Task<YResponse<YPins>> GetAsync(AuthStorage storage)
|
||||
=> new YGetPinsBuilder(api, storage).Build(null!).GetResponseAsync();
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Pins;
|
||||
using YandexMusic.API.Requests.Pins;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взаимодействия с прикреплёнными объектами
|
||||
/// </summary>
|
||||
public partial class YPinsAPI : YCommonAPI
|
||||
{
|
||||
|
||||
|
||||
public YPinsAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение списка прикреплённых объектов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPins>> GetAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetPinsBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
149
YandexMusic.API/API/YPlaylistAPI.cs
Normal file
149
YandexMusic.API/API/YPlaylistAPI.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Landing;
|
||||
using YandexMusic.API.Models.Landing.Entity.Entities;
|
||||
using YandexMusic.API.Models.Playlist;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Playlist;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>API для взаимодействия с плейлистами.</summary>
|
||||
public class YPlaylistAPI : YCommonAPI
|
||||
{
|
||||
/// <summary>Инициализирует новый экземпляр API плейлистов.</summary>
|
||||
/// <param name="yandex">Экземпляр основного API.</param>
|
||||
public YPlaylistAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||
|
||||
/// <summary>Получает список персональных плейлистов с главной страницы.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <returns>Список ответов с плейлистами.</returns>
|
||||
public async Task<List<YResponse<YPlaylist>>> GetPersonalPlaylistsAsync(AuthStorage storage)
|
||||
{
|
||||
var landing = await api.Landing.GetAsync(storage, YLandingBlockType.PersonalPlaylists);
|
||||
var block = landing.Result?.Blocks?.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists);
|
||||
if (block?.Entities == null)
|
||||
return new List<YResponse<YPlaylist>>();
|
||||
|
||||
var tasks = block.Entities
|
||||
.OfType<YLandingEntityPersonalPlaylist>()
|
||||
.Select(e => api.Playlist.GetAsync(storage, e.Data?.Data));
|
||||
|
||||
return new List<YResponse<YPlaylist>>(await Task.WhenAll(tasks));
|
||||
}
|
||||
|
||||
/// <summary>Получает избранные плейлисты.</summary>
|
||||
public Task<YResponse<List<YPlaylist>>> FavoritesAsync(AuthStorage storage)
|
||||
=> new YGetPlaylistFavoritesBuilder(api, storage).Build(null!).GetResponseAsync();
|
||||
|
||||
/// <summary>Получает плейлист дня.</summary>
|
||||
public Task<YResponse<YPlaylist>> OfTheDayAsync(AuthStorage storage)
|
||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.PlaylistOfTheDay);
|
||||
|
||||
/// <summary>Получает плейлист «Дежавю».</summary>
|
||||
public Task<YResponse<YPlaylist>> DejaVuAsync(AuthStorage storage)
|
||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.NeverHeard);
|
||||
|
||||
/// <summary>Получает плейлист «Премьера».</summary>
|
||||
public Task<YResponse<YPlaylist>> PremiereAsync(AuthStorage storage)
|
||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.RecentTracks);
|
||||
|
||||
/// <summary>Получает плейлист «Тайник».</summary>
|
||||
public Task<YResponse<YPlaylist>> MissedAsync(AuthStorage storage)
|
||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.MissedLikes);
|
||||
|
||||
/// <summary>Получает плейлист «Кинопоиск».</summary>
|
||||
public Task<YResponse<YPlaylist>> KinopoiskAsync(AuthStorage storage)
|
||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.Kinopoisk);
|
||||
|
||||
private async Task<YResponse<YPlaylist>> GetPersonalPlaylistAsync(AuthStorage storage, YGeneratedPlaylistType type)
|
||||
{
|
||||
var list = await GetPersonalPlaylistsAsync(storage);
|
||||
return list.FirstOrDefault(e => string.Equals(e.Result?.GeneratedPlaylistType, type.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
||||
?? throw new Exception($"Плейлист типа {type} не найден.");
|
||||
}
|
||||
|
||||
/// <summary>Получает плейлист по идентификатору пользователя и типа.</summary>
|
||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string user, string kind)
|
||||
=> new YGetPlaylistBuilder(api, storage).Build((user, kind)).GetResponseAsync();
|
||||
|
||||
/// <summary>Получает плейлист по UUID.</summary>
|
||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string uuid)
|
||||
=> new YGetPlaylistByUuidBuilder(api, storage).Build(uuid).GetResponseAsync();
|
||||
|
||||
/// <summary>Получает несколько плейлистов по списку пар (пользователь, тип).</summary>
|
||||
public Task<YResponse<List<YPlaylist>>> GetAsync(AuthStorage storage, IEnumerable<(string user, string kind)> ids)
|
||||
=> new YGetPlaylistsBuilder(api, storage).Build(ids).GetResponseAsync();
|
||||
|
||||
/// <summary>Получает плейлист по объекту плейлиста (обновляет его треки).</summary>
|
||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, YPlaylist playlist)
|
||||
=> new YGetPlaylistBuilder(api, storage).Build((playlist.Owner.Uid, playlist.Kind)).GetResponseAsync();
|
||||
|
||||
/// <summary>Создаёт новый плейлист с заданным именем.</summary>
|
||||
public Task<YResponse<YPlaylist>> CreateAsync(AuthStorage storage, string name)
|
||||
=> new YPlaylistCreateBuilder(api, storage).Build(name).GetResponseAsync();
|
||||
|
||||
/// <summary>Переименовывает плейлист.</summary>
|
||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, string kind, string name)
|
||||
=> new YPlaylistRenameBuilder(api, storage).Build((kind, name)).GetResponseAsync();
|
||||
|
||||
/// <summary>Переименовывает плейлист.</summary>
|
||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, YPlaylist playlist, string name)
|
||||
=> RenameAsync(storage, playlist.Kind, name);
|
||||
|
||||
/// <summary>Удаляет плейлист.</summary>
|
||||
public async Task<bool> DeleteAsync(AuthStorage storage, string kind)
|
||||
{
|
||||
try
|
||||
{
|
||||
await new YPlaylistRemoveBuilder(api, storage).Build(kind).GetResponseAsync();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Логирование ошибки можно добавить через ILogger
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Удаляет плейлист.</summary>
|
||||
public Task<bool> DeleteAsync(AuthStorage storage, YPlaylist playlist)
|
||||
=> DeleteAsync(storage, playlist.Kind);
|
||||
|
||||
/// <summary>Добавляет треки в начало плейлиста.</summary>
|
||||
public async Task<YResponse<YPlaylist>> InsertTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||
{
|
||||
var change = await ChangePlaylistAsync(storage, playlist, new List<YPlaylistChange>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Operation = YPlaylistChangeType.Insert,
|
||||
At = 0,
|
||||
Tracks = tracks.Select(t => t.GetKey())
|
||||
}
|
||||
});
|
||||
return await GetAsync(storage, change.Result);
|
||||
}
|
||||
|
||||
/// <summary>Удаляет треки из плейлиста.</summary>
|
||||
public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||
{
|
||||
var distinctTracks = tracks.Distinct().ToList();
|
||||
var changes = distinctTracks
|
||||
.Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1)
|
||||
.Where(i => i != -1)
|
||||
.Select(i => new YPlaylistChange
|
||||
{
|
||||
Operation = YPlaylistChangeType.Delete,
|
||||
From = i,
|
||||
To = i + 1,
|
||||
Tracks = new List<YTrackAlbumPair> { playlist.Tracks![i].Track!.GetKey() }
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return ChangePlaylistAsync(storage, playlist, changes);
|
||||
}
|
||||
|
||||
private Task<YResponse<YPlaylist>> ChangePlaylistAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YPlaylistChange> changes)
|
||||
=> new YPlaylistChangeBuilder(api, storage).Build((playlist, changes)).GetResponseAsync();
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Landing;
|
||||
using YandexMusic.API.Models.Landing.Entity.Entities;
|
||||
using YandexMusic.API.Models.Playlist;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Playlist;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взамодействия с плейлистами
|
||||
/// </summary>
|
||||
public partial class YPlaylistAPI : YCommonAPI
|
||||
{
|
||||
#region Вспомогательные функции
|
||||
|
||||
/// <summary>
|
||||
/// Получение персональных плейлистов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="type">Тип</param>
|
||||
/// <returns>Плейлист</returns>
|
||||
private async Task<YResponse<YPlaylist>> GetPersonalPlaylist(AuthStorage storage, YGeneratedPlaylistType type)
|
||||
{
|
||||
List<YResponse<YPlaylist>> list = await GetPersonalPlaylistsAsync(storage);
|
||||
return list.FirstOrDefault(e => string.Equals(e.Result.GeneratedPlaylistType, type.ToString(), StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Изменение плейлиста
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <param name="changes">Список изменений</param>
|
||||
/// <returns>Плейлист после изменений</returns>
|
||||
private Task<YResponse<YPlaylist>> ChangePlaylist(AuthStorage storage, YPlaylist playlist, IEnumerable<YPlaylistChange> changes)
|
||||
{
|
||||
return new YPlaylistChangeBuilder(api, storage)
|
||||
.Build((playlist, changes))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
private IEnumerable<YTrack> RemoveIdentical(IEnumerable<YTrack> tracks)
|
||||
{
|
||||
return tracks.Distinct();
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public YPlaylistAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
#region Список с главной
|
||||
|
||||
/// <summary>
|
||||
/// Получение списка персональных плейлистов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public async Task<List<YResponse<YPlaylist>>> GetPersonalPlaylistsAsync(AuthStorage storage)
|
||||
{
|
||||
YResponse<YLanding> landing = await api.Landing.GetAsync(storage, YLandingBlockType.PersonalPlaylists);
|
||||
|
||||
IEnumerable<Task<YResponse<YPlaylist>>> tasks = landing
|
||||
.Result
|
||||
.Blocks
|
||||
.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists)
|
||||
?.Entities
|
||||
.Select(e => api.Playlist.GetAsync(storage, ((YLandingEntityPersonalPlaylist)e).Data?.Data));
|
||||
|
||||
return tasks == null
|
||||
? new List<YResponse<YPlaylist>>()
|
||||
: new List<YResponse<YPlaylist>>(await Task.WhenAll(tasks));
|
||||
}
|
||||
|
||||
#endregion Список с главной
|
||||
|
||||
#region Стандартные плейлисты
|
||||
|
||||
/// <summary>
|
||||
/// Избранное
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YPlaylist>>> FavoritesAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetPlaylistFavoritesBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Плейлист дня
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> OfTheDayAsync(AuthStorage storage)
|
||||
{
|
||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.PlaylistOfTheDay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Дежавю
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> DejaVuAsync(AuthStorage storage)
|
||||
{
|
||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.NeverHeard);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Премьера
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> PremiereAsync(AuthStorage storage)
|
||||
{
|
||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.RecentTracks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Тайник
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> MissedAsync(AuthStorage storage)
|
||||
{
|
||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.MissedLikes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Кинопоиск
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> KinopoiskAsync(AuthStorage storage)
|
||||
{
|
||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.Kinopoisk);
|
||||
}
|
||||
|
||||
#endregion Стандартные плейлисты
|
||||
|
||||
#region Получение плейлиста
|
||||
|
||||
/// <summary>
|
||||
/// Получение плейлиста
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="user">Uid пользователя-владельца плейлиста</param>
|
||||
/// <param name="kind">Тип</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string user, string kind)
|
||||
{
|
||||
return new YGetPlaylistBuilder(api, storage)
|
||||
.Build((user, kind))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение плейлиста по uuid
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="uuid">uuid</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string uuid)
|
||||
{
|
||||
return new YGetPlaylistByUuidBuilder(api, storage)
|
||||
.Build(uuid)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение плейлистов
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="ids">Список пар пользователь:тип</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YPlaylist>>> GetAsync(AuthStorage storage, IEnumerable<(string user, string kind)> ids)
|
||||
{
|
||||
return new YGetPlaylistsBuilder(api, storage)
|
||||
.Build(ids)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение плейлиста
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Описание плейлиста, для которого будут запрошены треки</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, YPlaylist playlist)
|
||||
{
|
||||
return new YGetPlaylistBuilder(api, storage)
|
||||
.Build((playlist.Owner.Uid, playlist.Kind))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion Получение плейлиста
|
||||
|
||||
#region Операции над плейлистами
|
||||
|
||||
/// <summary>
|
||||
/// Создание
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="name">Заголовок</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> CreateAsync(AuthStorage storage, string name)
|
||||
{
|
||||
return new YPlaylistCreateBuilder(api, storage)
|
||||
.Build(name)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переименование
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="kinds">Идентификатор плейлиста</param>
|
||||
/// <param name="name">Заголовок</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, string kinds, string name)
|
||||
{
|
||||
return new YPlaylistRenameBuilder(api, storage)
|
||||
.Build((kinds, name))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переименование
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <param name="name">Заголовок</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, YPlaylist playlist, string name)
|
||||
{
|
||||
return RenameAsync(storage, playlist.Kind, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаление
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="kinds">Тип</param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> DeleteAsync(AuthStorage storage, string kinds)
|
||||
{
|
||||
try
|
||||
{
|
||||
await new YPlaylistRemoveBuilder(api, storage)
|
||||
.Build(kinds)
|
||||
.GetResponseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаление
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <returns></returns>
|
||||
public Task<bool> DeleteAsync(AuthStorage storage, YPlaylist playlist)
|
||||
{
|
||||
return DeleteAsync(storage, playlist.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавление трека
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <param name="tracks">Треки для добавления</param>
|
||||
/// <returns></returns>
|
||||
public async Task<YResponse<YPlaylist>> InsertTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||
{
|
||||
YResponse<YPlaylist> change = await ChangePlaylist(storage, playlist, new List<YPlaylistChange> {
|
||||
new() {
|
||||
Operation = YPlaylistChangeType.Insert,
|
||||
At = 0,
|
||||
Tracks = tracks.Select(t => t.GetKey())
|
||||
}
|
||||
});
|
||||
|
||||
return await GetAsync(storage, change.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаление треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlist">Плейлист</param>
|
||||
/// <param name="tracks">Треки для удаления</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||
{
|
||||
List<YPlaylistChange> changes = RemoveIdentical(tracks)
|
||||
.Select(t => playlist.Tracks.Select(c => c.Track).ToList().IndexOf(t))
|
||||
.Where(i => i != -1)
|
||||
.Select(i =>
|
||||
{
|
||||
YTrackContainer t = playlist.Tracks[i];
|
||||
return new YPlaylistChange
|
||||
{
|
||||
Operation = YPlaylistChangeType.Delete,
|
||||
From = i,
|
||||
To = i + 1,
|
||||
Tracks = new List<YTrackAlbumPair> {
|
||||
t.Track.GetKey()
|
||||
}
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return ChangePlaylist(storage, playlist, changes);
|
||||
}
|
||||
|
||||
#endregion Операции над плейлистами
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
72
YandexMusic.API/API/YQueueAPI.cs
Normal file
72
YandexMusic.API/API/YQueueAPI.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Queue;
|
||||
using YandexMusic.API.Requests.Queue;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// API для взаимодействия с очередями
|
||||
/// </summary>
|
||||
public partial class YQueueAPI : YCommonAPI
|
||||
{
|
||||
public YQueueAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение всех очередей треков с разных устройств для синхронизации между ними
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="device">Устройство</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YQueueItemsContainer>> ListAsync(AuthStorage storage, string? device = null)
|
||||
{
|
||||
return new YQueuesListBuilder(api, storage)
|
||||
.Build(device)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение очереди
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="queueId">Идентификатор очереди</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YQueue>> GetAsync(AuthStorage storage, string queueId)
|
||||
{
|
||||
return new YGetQueueBuilder(api, storage)
|
||||
.Build(queueId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создание новой очереди треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="queue">Очередь треков</param>
|
||||
/// <param name="device">Устройство</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YNewQueue>> CreateAsync(AuthStorage storage, YQueue queue, string? device = null)
|
||||
{
|
||||
return new YQueueCreateBuilder(api, storage, device)
|
||||
.Build(queue)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка текущего индекса проигрываемого трека в очереди треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="queueId">Идентификатор очереди</param>
|
||||
/// <param name="currentIndex">Текущий индекс</param>
|
||||
/// <param name="isInteractive">Флаг интерактивности</param>
|
||||
/// <param name="device">Устройство</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YUpdatedQueue>> UpdatePositionAsync(AuthStorage storage, string queueId, int currentIndex, bool isInteractive, string device = null)
|
||||
{
|
||||
return new YQueueUpdatePositionBuilder(api, storage, device)
|
||||
.Build((queueId, currentIndex, isInteractive))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Queue;
|
||||
using YandexMusic.API.Requests.Queue;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взаимодействия с очередями
|
||||
/// </summary>
|
||||
public partial class YQueueAPI : YCommonAPI
|
||||
{
|
||||
public YQueueAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение всех очередей треков с разных устройств для синхронизации между ними
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="device">Устройство</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YQueueItemsContainer>> ListAsync(AuthStorage storage, string device = null)
|
||||
{
|
||||
return new YQueuesListBuilder(api, storage)
|
||||
.Build(device)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение очереди
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="queueId">Идентификатор очереди</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YQueue>> GetAsync(AuthStorage storage, string queueId)
|
||||
{
|
||||
return new YGetQueueBuilder(api, storage)
|
||||
.Build(queueId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создание новой очереди треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="queue">Очередь треков</param>
|
||||
/// <param name="device">Устройство</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YNewQueue>> CreateAsync(AuthStorage storage, YQueue queue, string device = null)
|
||||
{
|
||||
return new YQueueCreateBuilder(api, storage, device)
|
||||
.Build(queue)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка текущего индекса проигрываемого трека в очереди треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="queueId">Идентификатор очереди</param>
|
||||
/// <param name="currentIndex">Текущий индекс</param>
|
||||
/// <param name="isInteractive">Флаг интерактивности</param>
|
||||
/// <param name="device">Устройство</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YUpdatedQueue>> UpdatePositionAsync(AuthStorage storage, string queueId, int currentIndex, bool isInteractive, string device = null)
|
||||
{
|
||||
return new YQueueUpdatePositionBuilder(api, storage, device)
|
||||
.Build((queueId, currentIndex, isInteractive))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
113
YandexMusic.API/API/YRadioAPI.cs
Normal file
113
YandexMusic.API/API/YRadioAPI.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Radio;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Radio;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// API для взаимодействия с радио
|
||||
/// </summary>
|
||||
public partial class YRadioAPI : YCommonAPI
|
||||
{
|
||||
public YRadioAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение списка рекомендованных радиостанций
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YStationsDashboard>> GetStationsDashboardAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetStationsDashboardBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение списка радиостанций
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YStation>>> GetStationsAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetStationsBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации о радиостанции
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="type">Тип</param>
|
||||
/// <param name="tag">Тэг</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, string type, string tag)
|
||||
{
|
||||
return new YGetStationBuilder(api, storage)
|
||||
.Build((type, tag))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации о радиостанции
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="id">Идентификатор станции</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, YStationId id)
|
||||
{
|
||||
return GetStationAsync(storage, id.Type, id.Tag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение последовательности треков радиостанции
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="station">Радиостанция</param>
|
||||
/// <param name="prevTrackId">Идентификатор предыдущего трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YStationSequence>> GetStationTracksAsync(AuthStorage storage, YStation station, string prevTrackId = "")
|
||||
{
|
||||
return new YGetStationTracksBuilder(api, storage)
|
||||
.Build((station.Station, prevTrackId))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка настроек подбора треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="station">Радиостанция</param>
|
||||
/// <param name="settings">Настройки</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> SetStationSettings2Async(AuthStorage storage, YStation station, YStationSettings2 settings)
|
||||
{
|
||||
return new YSetSettings2Builder(api, storage)
|
||||
.Build((station.Station, settings))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка обратной связи на действия при прослушивании радио
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="station">Радиостанция</param>
|
||||
/// <param name="type">Тип обратной связи</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="batchId">Уникальный идентификатор партии треков. Возвращается при получении треков</param>
|
||||
/// <param name="totalPlayedSeconds">Сколько было проиграно секунд трека перед действием</param>
|
||||
/// <returns></returns>
|
||||
public Task<string> SendStationFeedBackAsync(AuthStorage storage, YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
|
||||
{
|
||||
return new YSetStationFeedbackBuilder(api, storage)
|
||||
.Build((type, station, track, batchId, totalPlayedSeconds))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Radio;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Radio;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взаимодействия с радио
|
||||
/// </summary>
|
||||
public partial class YRadioAPI : YCommonAPI
|
||||
{
|
||||
|
||||
|
||||
public YRadioAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение списка рекомендованных радиостанций
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YStationsDashboard>> GetStationsDashboardAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetStationsDashboardBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение списка радиостанций
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YStation>>> GetStationsAsync(AuthStorage storage)
|
||||
{
|
||||
return new YGetStationsBuilder(api, storage)
|
||||
.Build(null)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации о радиостанции
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="type">Тип</param>
|
||||
/// <param name="tag">Тэг</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, string type, string tag)
|
||||
{
|
||||
return new YGetStationBuilder(api, storage)
|
||||
.Build((type, tag))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации о радиостанции
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="id">Идентификатор станции</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, YStationId id)
|
||||
{
|
||||
return GetStationAsync(storage, id.Type, id.Tag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение последовательности треков радиостанции
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="station">Радиостанция</param>
|
||||
/// <param name="prevTrackId">Идентификатор предыдущего трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YStationSequence>> GetStationTracksAsync(AuthStorage storage, YStation station, string prevTrackId = "")
|
||||
{
|
||||
return new YGetStationTracksBuilder(api, storage)
|
||||
.Build((station.Station, prevTrackId))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка настроек подбора треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="station">Радиостанция</param>
|
||||
/// <param name="settings">Настройки</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<string>> SetStationSettings2Async(AuthStorage storage, YStation station, YStationSettings2 settings)
|
||||
{
|
||||
return new YSetSettings2Builder(api, storage)
|
||||
.Build((station.Station, settings))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка обратной связи на действия при прослушивании радио
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="station">Радиостанция</param>
|
||||
/// <param name="type">Тип обратной связи</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="batchId">Уникальный идентификатор партии треков. Возвращается при получении треков</param>
|
||||
/// <param name="totalPlayedSeconds">Сколько было проиграно секунд трека перед действием</param>
|
||||
/// <returns></returns>
|
||||
public Task<string> SendStationFeedBackAsync(AuthStorage storage, YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
|
||||
{
|
||||
return new YSetStationFeedbackBuilder(api, storage)
|
||||
.Build((type, station, track, batchId, totalPlayedSeconds))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
139
YandexMusic.API/API/YSearchAPI.cs
Normal file
139
YandexMusic.API/API/YSearchAPI.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Search;
|
||||
using YandexMusic.API.Requests.Search;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// API для поиска
|
||||
/// </summary>
|
||||
public partial class YSearchAPI : YCommonAPI
|
||||
{
|
||||
|
||||
public YSearchAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по трекам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackName">Имя трека</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> TrackAsync(AuthStorage storage, string trackName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, trackName, YSearchType.Track, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по альбомам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="albumName">Имя альбома</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> AlbumsAsync(AuthStorage storage, string albumName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, albumName, YSearchType.Album, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по артисту
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="artistName">Имя артиста</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> ArtistAsync(AuthStorage storage, string artistName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, artistName, YSearchType.Artist, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по плейлистам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlistName">Имя плейлиста</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> PlaylistAsync(AuthStorage storage, string playlistName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, playlistName, YSearchType.Playlist, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по плейлистам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="podcastName">Имя подкаста</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> PodcastEpisodeAsync(AuthStorage storage, string podcastName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, podcastName, YSearchType.PodcastEpisode, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по видео
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="videoName">Имя видео</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> VideosAsync(AuthStorage storage, string videoName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, videoName, YSearchType.Video, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по пользователям
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="userName">Имя пользователя</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> UsersAsync(AuthStorage storage, string userName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, userName, YSearchType.User, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="searchText">Поисковый запрос</param>
|
||||
/// <param name="searchType">Тип поиска</param>
|
||||
/// <param name="page">Страница</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> SearchAsync(AuthStorage storage, string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
|
||||
{
|
||||
return new YSearchBuilder(api, storage)
|
||||
.Build((searchText, searchType, page, pageSize))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подсказка
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="searchText">Поисковый запрос</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearchSuggest>> SuggestAsync(AuthStorage storage, string searchText)
|
||||
{
|
||||
return new YSearchSuggestBuilder(api, storage)
|
||||
.Build(searchText)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Search;
|
||||
using YandexMusic.API.Requests.Search;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для поиска
|
||||
/// </summary>
|
||||
public partial class YSearchAPI : YCommonAPI
|
||||
{
|
||||
|
||||
|
||||
public YSearchAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по трекам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackName">Имя трека</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> TrackAsync(AuthStorage storage, string trackName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, trackName, YSearchType.Track, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по альбомам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="albumName">Имя альбома</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> AlbumsAsync(AuthStorage storage, string albumName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, albumName, YSearchType.Album, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по артисту
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="artistName">Имя артиста</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> ArtistAsync(AuthStorage storage, string artistName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, artistName, YSearchType.Artist, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по плейлистам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="playlistName">Имя плейлиста</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> PlaylistAsync(AuthStorage storage, string playlistName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, playlistName, YSearchType.Playlist, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по плейлистам
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="podcastName">Имя подкаста</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> PodcastEpisodeAsync(AuthStorage storage, string podcastName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, podcastName, YSearchType.PodcastEpisode, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по видео
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="videoName">Имя видео</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> VideosAsync(AuthStorage storage, string videoName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, videoName, YSearchType.Video, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск по пользователям
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="userName">Имя пользователя</param>
|
||||
/// <param name="pageNumber">Номер страницы</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> UsersAsync(AuthStorage storage, string userName, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
return SearchAsync(storage, userName, YSearchType.User, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="searchText">Поисковый запрос</param>
|
||||
/// <param name="searchType">Тип поиска</param>
|
||||
/// <param name="page">Страница</param>
|
||||
/// <param name="pageSize">Размер страницы</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearch>> SearchAsync(AuthStorage storage, string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
|
||||
{
|
||||
return new YSearchBuilder(api, storage)
|
||||
.Build((searchText, searchType, page, pageSize))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подсказка
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="searchText">Поисковый запрос</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YSearchSuggest>> SuggestAsync(AuthStorage storage, string searchText)
|
||||
{
|
||||
return new YSearchSuggestBuilder(api, storage)
|
||||
.Build(searchText)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
307
YandexMusic.API/API/YTrackAPI.cs
Normal file
307
YandexMusic.API/API/YTrackAPI.cs
Normal file
@@ -0,0 +1,307 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Track;
|
||||
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// API для взаимодействия с треками
|
||||
/// </summary>
|
||||
public partial class YTrackAPI : YCommonAPI
|
||||
{
|
||||
#region Вспомогательные функции
|
||||
|
||||
private string BuildLinkForDownload(YTrackDownloadInfo mainDownloadResponse, YStorageDownloadFile storageDownload)
|
||||
{
|
||||
string path = storageDownload.Path;
|
||||
string host = storageDownload.Host;
|
||||
string ts = storageDownload.Ts;
|
||||
string s = storageDownload.S;
|
||||
string codec = mainDownloadResponse.Codec;
|
||||
|
||||
string secret = $"XGRlBW9FXlekgbPrRHuSiA{path.Substring(1, path.Length - 1)}{s}";
|
||||
MD5 md5 = MD5.Create();
|
||||
byte[] md5Hash = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
|
||||
HMACSHA1 hmacsha1 = new();
|
||||
byte[] hmasha1Hash = hmacsha1.ComputeHash(md5Hash);
|
||||
string sign = BitConverter.ToString(hmasha1Hash).Replace("-", "").ToLower();
|
||||
|
||||
string link = $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public YTrackAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackId">Идентификатор трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, string trackId)
|
||||
{
|
||||
return new YGetTracksBuilder(api, storage)
|
||||
.Build(new[] { trackId })
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackIds">Идентификаторы треков</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, IEnumerable<string> trackIds)
|
||||
{
|
||||
return new YGetTracksBuilder(api, storage)
|
||||
.Build(trackIds)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение метаданных для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентифактор трека:идентификатор альбома}</param>
|
||||
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, string trackKey, bool direct = false)
|
||||
{
|
||||
return new YTrackDownloadInfoBuilder(api, storage)
|
||||
.Build((trackKey, direct))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение метаданных для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, YTrack track, bool direct = false)
|
||||
{
|
||||
return GetMetadataForDownloadAsync(storage, track.GetKey().ToString(), direct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации для формирования ссылки для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="metadataInfo">Метаданные для загрузки</param>
|
||||
/// <returns></returns>
|
||||
public Task<YStorageDownloadFile> GetDownloadFileInfoAsync(AuthStorage storage, YTrackDownloadInfo metadataInfo)
|
||||
{
|
||||
return new YStorageDownloadFileBuilder(api, storage)
|
||||
.Build(metadataInfo.DownloadInfoUrl)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение ссылки для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetFileLinkAsync(AuthStorage storage, string trackKey)
|
||||
{
|
||||
YResponse<List<YTrackDownloadInfo>> meta = await GetMetadataForDownloadAsync(storage, trackKey);
|
||||
YTrackDownloadInfo info = meta.Result
|
||||
.OrderByDescending(i => i.BitrateInKbps)
|
||||
.First(m => m.Codec == "mp3");
|
||||
YStorageDownloadFile storageDownload = await GetDownloadFileInfoAsync(storage, info);
|
||||
return BuildLinkForDownload(info, storageDownload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение ссылки для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<string> GetFileLinkAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return GetFileLinkAsync(storage, track.GetKey().ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка текущего состояния прослушиваемого трека
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="from">Наименования клиента, с которого происходит прослушивание</param>
|
||||
/// <param name="fromCache">Проигрывается ли трек с кеша</param>
|
||||
/// <param name="playId">Уникальный идентификатор проигрывания</param>
|
||||
/// <param name="playlistId">Уникальный идентификатор плейлиста, если таковой прослушивается</param>
|
||||
/// <param name="totalPlayedSeconds">Сколько было всего воспроизведено трека в секундах</param>
|
||||
/// <param name="endPositionSeconds">Окончательное значение воспроизведенных секунд</param>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<string> SendPlayTrackInfoAsync(AuthStorage storage, YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
|
||||
{
|
||||
return new YSendTrackInfoBuilder(api, storage)
|
||||
.Build((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#region GetSupplement
|
||||
|
||||
/// <summary>
|
||||
/// Получение дополнительной информации для трека
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackId">Идентификатор трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, string trackId)
|
||||
{
|
||||
return new YGetTrackSupplementBuilder(api, storage)
|
||||
.Build(trackId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение дополнительной информации для трека
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YGetTrackSupplementBuilder(api, storage)
|
||||
.Build(track.GetKey().ToString())
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion GetSupplement
|
||||
|
||||
#region GetSimilar
|
||||
|
||||
/// <summary>
|
||||
/// Получение похожих треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackId">Идентификатор трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, string trackId)
|
||||
{
|
||||
return new YGetTrackSimilarBuilder(api, storage)
|
||||
.Build(trackId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение похожих треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YGetTrackSimilarBuilder(api, storage)
|
||||
.Build(track.GetKey().ToString())
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion GetSimilar
|
||||
|
||||
#region Получение данных трека
|
||||
|
||||
#region В файл
|
||||
|
||||
/// <summary>
|
||||
/// Выгрузка в файл
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <param name="filePath">Путь для файла</param>
|
||||
public async Task ExtractToFileAsync(AuthStorage storage, string trackKey, string filePath)
|
||||
{
|
||||
string url = await GetFileLinkAsync(storage, trackKey);
|
||||
await new DataDownloader(storage).ToFile(url, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выгрузка в файл
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="filePath">Путь для файла</param>
|
||||
public Task ExtractToFileAsync(AuthStorage storage, YTrack track, string filePath)
|
||||
{
|
||||
return ExtractToFileAsync(storage, track.GetKey().ToString(), filePath);
|
||||
}
|
||||
|
||||
#endregion В файл
|
||||
|
||||
#region В массив байт
|
||||
|
||||
/// <summary>
|
||||
/// Получение двоичного массива данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <returns></returns>
|
||||
public async Task<byte[]> ExtractDataAsync(AuthStorage storage, string trackKey)
|
||||
{
|
||||
string url = await GetFileLinkAsync(storage, trackKey);
|
||||
return await new DataDownloader(storage).AsBytes(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение двоичного массива данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<byte[]> ExtractDataAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return ExtractDataAsync(storage, track.GetKey().ToString());
|
||||
}
|
||||
|
||||
#endregion В массив байт
|
||||
|
||||
#region В поток
|
||||
|
||||
/// <summary>
|
||||
/// Получение потока данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
|
||||
/// <returns></returns>
|
||||
public async Task<Stream> ExtractStreamAsync(AuthStorage storage, string trackKey,
|
||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
string url = await GetFileLinkAsync(storage, trackKey);
|
||||
return await new DataDownloader(storage).AsStream(url, httpCompletionOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение потока данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
|
||||
/// <returns></returns>
|
||||
public Task<Stream> ExtractStreamAsync(AuthStorage storage, YTrack track,
|
||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
return ExtractStreamAsync(storage, track.GetKey().ToString(), httpCompletionOption);
|
||||
}
|
||||
|
||||
#endregion В поток
|
||||
|
||||
#endregion Получение данных трека
|
||||
|
||||
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Models.Common;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Requests.Track;
|
||||
|
||||
namespace YandexMusic.API
|
||||
{
|
||||
/// <summary>
|
||||
/// API для взаимодействия с треками
|
||||
/// </summary>
|
||||
public partial class YTrackAPI : YCommonAPI
|
||||
{
|
||||
#region Вспомогательные функции
|
||||
|
||||
private string BuildLinkForDownload(YTrackDownloadInfo mainDownloadResponse, YStorageDownloadFile storageDownload)
|
||||
{
|
||||
string path = storageDownload.Path;
|
||||
string host = storageDownload.Host;
|
||||
string ts = storageDownload.Ts;
|
||||
string s = storageDownload.S;
|
||||
string codec = mainDownloadResponse.Codec;
|
||||
|
||||
string secret = $"XGRlBW9FXlekgbPrRHuSiA{path.Substring(1, path.Length - 1)}{s}";
|
||||
MD5 md5 = MD5.Create();
|
||||
byte[] md5Hash = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
|
||||
HMACSHA1 hmacsha1 = new();
|
||||
byte[] hmasha1Hash = hmacsha1.ComputeHash(md5Hash);
|
||||
string sign = BitConverter.ToString(hmasha1Hash).Replace("-", "").ToLower();
|
||||
|
||||
string link = $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public YTrackAPI(YandexMusicApi yandex) : base(yandex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackId">Идентификатор трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, string trackId)
|
||||
{
|
||||
return new YGetTracksBuilder(api, storage)
|
||||
.Build(new[] { trackId })
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackIds">Идентификаторы треков</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, IEnumerable<string> trackIds)
|
||||
{
|
||||
return new YGetTracksBuilder(api, storage)
|
||||
.Build(trackIds)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение метаданных для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентифактор трека:идентификатор альбома}</param>
|
||||
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, string trackKey, bool direct = false)
|
||||
{
|
||||
return new YTrackDownloadInfoBuilder(api, storage)
|
||||
.Build((trackKey, direct))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение метаданных для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, YTrack track, bool direct = false)
|
||||
{
|
||||
return GetMetadataForDownloadAsync(storage, track.GetKey().ToString(), direct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации для формирования ссылки для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="metadataInfo">Метаданные для загрузки</param>
|
||||
/// <returns></returns>
|
||||
public Task<YStorageDownloadFile> GetDownloadFileInfoAsync(AuthStorage storage, YTrackDownloadInfo metadataInfo)
|
||||
{
|
||||
return new YStorageDownloadFileBuilder(api, storage)
|
||||
.Build(metadataInfo.DownloadInfoUrl)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение ссылки для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetFileLinkAsync(AuthStorage storage, string trackKey)
|
||||
{
|
||||
YResponse<List<YTrackDownloadInfo>> meta = await GetMetadataForDownloadAsync(storage, trackKey);
|
||||
YTrackDownloadInfo info = meta.Result
|
||||
.OrderByDescending(i => i.BitrateInKbps)
|
||||
.First(m => m.Codec == "mp3");
|
||||
YStorageDownloadFile storageDownload = await GetDownloadFileInfoAsync(storage, info);
|
||||
return BuildLinkForDownload(info, storageDownload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение ссылки для загрузки
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<string> GetFileLinkAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return GetFileLinkAsync(storage, track.GetKey().ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка текущего состояния прослушиваемого трека
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="from">Наименования клиента, с которого происходит прослушивание</param>
|
||||
/// <param name="fromCache">Проигрывается ли трек с кеша</param>
|
||||
/// <param name="playId">Уникальный идентификатор проигрывания</param>
|
||||
/// <param name="playlistId">Уникальный идентификатор плейлиста, если таковой прослушивается</param>
|
||||
/// <param name="totalPlayedSeconds">Сколько было всего воспроизведено трека в секундах</param>
|
||||
/// <param name="endPositionSeconds">Окончательное значение воспроизведенных секунд</param>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<string> SendPlayTrackInfoAsync(AuthStorage storage, YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
|
||||
{
|
||||
return new YSendTrackInfoBuilder(api, storage)
|
||||
.Build((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds))
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#region GetSupplement
|
||||
|
||||
/// <summary>
|
||||
/// Получение дополнительной информации для трека
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackId">Идентификатор трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, string trackId)
|
||||
{
|
||||
return new YGetTrackSupplementBuilder(api, storage)
|
||||
.Build(trackId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение дополнительной информации для трека
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YGetTrackSupplementBuilder(api, storage)
|
||||
.Build(track.GetKey().ToString())
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion GetSupplement
|
||||
|
||||
#region GetSimilar
|
||||
|
||||
/// <summary>
|
||||
/// Получение похожих треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackId">Идентификатор трека</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, string trackId)
|
||||
{
|
||||
return new YGetTrackSimilarBuilder(api, storage)
|
||||
.Build(trackId)
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение похожих треков
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return new YGetTrackSimilarBuilder(api, storage)
|
||||
.Build(track.GetKey().ToString())
|
||||
.GetResponseAsync();
|
||||
}
|
||||
|
||||
#endregion GetSimilar
|
||||
|
||||
#region Получение данных трека
|
||||
|
||||
#region В файл
|
||||
|
||||
/// <summary>
|
||||
/// Выгрузка в файл
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <param name="filePath">Путь для файла</param>
|
||||
public async Task ExtractToFileAsync(AuthStorage storage, string trackKey, string filePath)
|
||||
{
|
||||
string url = await GetFileLinkAsync(storage, trackKey);
|
||||
await new DataDownloader(storage).ToFile(url, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выгрузка в файл
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="filePath">Путь для файла</param>
|
||||
public Task ExtractToFileAsync(AuthStorage storage, YTrack track, string filePath)
|
||||
{
|
||||
return ExtractToFileAsync(storage, track.GetKey().ToString(), filePath);
|
||||
}
|
||||
|
||||
#endregion В файл
|
||||
|
||||
#region В массив байт
|
||||
|
||||
/// <summary>
|
||||
/// Получение двоичного массива данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <returns></returns>
|
||||
public async Task<byte[]> ExtractDataAsync(AuthStorage storage, string trackKey)
|
||||
{
|
||||
string url = await GetFileLinkAsync(storage, trackKey);
|
||||
return await new DataDownloader(storage).AsBytes(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение двоичного массива данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <returns></returns>
|
||||
public Task<byte[]> ExtractDataAsync(AuthStorage storage, YTrack track)
|
||||
{
|
||||
return ExtractDataAsync(storage, track.GetKey().ToString());
|
||||
}
|
||||
|
||||
#endregion В массив байт
|
||||
|
||||
#region В поток
|
||||
|
||||
/// <summary>
|
||||
/// Получение потока данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
||||
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
|
||||
/// <returns></returns>
|
||||
public async Task<Stream> ExtractStreamAsync(AuthStorage storage, string trackKey,
|
||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
string url = await GetFileLinkAsync(storage, trackKey);
|
||||
return await new DataDownloader(storage).AsStream(url, httpCompletionOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение потока данных
|
||||
/// </summary>
|
||||
/// <param name="storage">Хранилище</param>
|
||||
/// <param name="track">Трек</param>
|
||||
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
|
||||
/// <returns></returns>
|
||||
public Task<Stream> ExtractStreamAsync(AuthStorage storage, YTrack track,
|
||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
return ExtractStreamAsync(storage, track.GetKey().ToString(), httpCompletionOption);
|
||||
}
|
||||
|
||||
#endregion В поток
|
||||
|
||||
#endregion Получение данных трека
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user