search feature

This commit is contained in:
ju09279
2024-08-18 02:20:46 +02:00
parent b4ac10c006
commit 97521f19e5
13 changed files with 783 additions and 88 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
################################################################################
# Diese .gitignore-Datei wurde von Microsoft(R) Visual Studio automatisch erstellt.
################################################################################
/backend/Beatmaps.db

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using shitweb;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Drawing.Imaging; using System.Drawing.Imaging;
@@ -39,12 +40,12 @@ app.MapGet("/api/v1/songs/recent", (int? limit, int? offset) =>
var limitValue = limit ?? 100; // default to 10 if not provided var limitValue = limit ?? 100; // default to 10 if not provided
var offsetValue = offset ?? 0; // default to 0 if not provided var offsetValue = offset ?? 0; // default to 0 if not provided
return Results.Json(Osudb.Instance.GetRecent(limitValue, offsetValue)); return Results.Json(SqliteDB.GetByRecent(limitValue, offsetValue));
}); });
app.MapGet("/api/v1/songs/favorite", (int? limit, int? offset) => app.MapGet("/api/v1/songs/favorite", (int? limit, int? offset) =>
{ {
var limitValue = limit ?? 10; // default to 10 if not provided var limitValue = limit ?? 100; // default to 10 if not provided
var offsetValue = offset ?? 0; // default to 0 if not provided var offsetValue = offset ?? 0; // default to 0 if not provided
return Results.Ok(new { Limit = limitValue, Offset = offsetValue, Message = "List of favorite songs" }); return Results.Ok(new { Limit = limitValue, Offset = offsetValue, Message = "List of favorite songs" });
@@ -56,13 +57,17 @@ app.MapGet("/api/v1/songs/{hash}", (string hash) =>
}); });
app.MapGet("/api/v1/collections/", async (int? limit, int? offset, [FromServices] IMemoryCache cache) => app.MapGet("/api/v1/collections/", async (int? limit, int? offset, [FromServices] IMemoryCache cache) =>
{ {
const string cacheKey = "collections";
var limitValue = limit ?? 100; // default to 10 if not provided
var offsetValue = offset ?? 0;
string cacheKey = $"collections_{offsetValue}_{limitValue}";
if (!cache.TryGetValue(cacheKey, out var collections)) if (!cache.TryGetValue(cacheKey, out var collections))
{ {
collections = Osudb.Instance.GetCollections(); collections = Osudb.Instance.GetCollections(limit: limitValue, offset: offsetValue);
var cacheEntryOptions = new MemoryCacheEntryOptions() var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromDays(1)) .SetSlidingExpiration(TimeSpan.FromDays(1))
@@ -105,6 +110,28 @@ app.MapGet("/api/v1/audio/{*fileName}", async (string fileName, HttpContext cont
return Results.Stream(fileStream, contentType, enableRangeProcessing: true); return Results.Stream(fileStream, contentType, enableRangeProcessing: true);
}); });
app.MapGet("/api/v1/search/active", async (string? q) =>
{
return Results.Ok(SqliteDB.activeSearch(q));
});
app.MapGet("/api/v1/search/artist", async (string? q, int? limit, int? offset) =>
{
var limitv = limit ?? 100;
var offsetv = offset ?? 0;
return Results.Ok(SqliteDB.GetArtistSearch(q, limitv, offsetv));
});
app.MapGet("/api/v1/search/songs", async (string? q, int? limit, int? offset) =>
{
return Results.Ok();
});
app.MapGet("/api/v1/search/collections", async (string? q, int? limit, int? offset) =>
{
return Results.Ok();
});
app.MapGet("/api/v1/images/{*filename}", async (string filename, int? h, int? w) => app.MapGet("/api/v1/images/{*filename}", async (string filename, int? h, int? w) =>
{ {
@@ -195,4 +222,5 @@ static ImageFormat GetImageFormat(string extension)
}; };
} }
app.Run(); Osudb.Instance.ToString();
app.Run();

444
backend/SqliteDB.cs Normal file
View File

@@ -0,0 +1,444 @@
using OsuParsers.Beatmaps;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SQLite;
using System.Reflection.PortableExecutable;
namespace shitweb
{
public class SqliteDB
{
private static SqliteDB instance;
private const string filename = "Beatmaps.db";
private const string dburl = $"Data Source={filename};Version=3;";
static SqliteDB() { }
public static SqliteDB Instance()
{
if (instance == null)
{
instance = new SqliteDB();
}
return instance;
}
public void setup(OsuParsers.Database.OsuDatabase osuDatabase)
{
int count = 0;
using (var connection = new SQLiteConnection(dburl))
{
connection.Open();
string createBeatmaps = @"
CREATE TABLE IF NOT EXISTS Beatmap (
BeatmapId INTEGER DEFAULT 0,
Artist TEXT DEFAULT '?????',
ArtistUnicode TEXT DEFAULT '?????',
Title TEXT DEFAULT '???????',
TitleUnicode TEXT DEFAULT '???????',
Creator TEXT DEFAULT '?????',
Difficulty TEXT DEFAULT '1',
AudioFileName TEXT DEFAULT 'unknown.mp3',
MD5Hash TEXT DEFAULT '00000000000000000000000000000000',
FileName TEXT DEFAULT 'unknown.osu',
RankedStatus TEXT DEFAULT Unknown,
LastModifiedTime DATETIME DEFAULT '0001-01-01 00:00:00',
TotalTime INTEGER DEFAULT 0,
AudioPreviewTime INTEGER DEFAULT 0,
BeatmapSetId INTEGER DEFAULT -1,
Source TEXT DEFAULT '',
Tags TEXT DEFAULT '',
LastPlayed DATETIME DEFAULT '0001-01-01 00:00:00',
FolderName TEXT DEFAULT 'Unknown Folder',
UNIQUE (Artist, Title, MD5Hash)
);";
using (var command = new SQLiteCommand(createBeatmaps, connection))
{
command.ExecuteNonQuery();
}
string activeSearch = @"CREATE VIRTUAL TABLE IF NOT EXISTS BeatmapFTS USING fts4(
Title,
Artist,
);";
using (var command = new SQLiteCommand(activeSearch, connection))
{
command.ExecuteNonQuery();
}
string triggerSearchupdate = @"CREATE TRIGGER IF NOT EXISTS Beatmap_Insert_Trigger
AFTER INSERT ON Beatmap
BEGIN
INSERT INTO BeatmapFTS (Title, Artist)
VALUES (NEW.Title, NEW.Artist);
END;";
using (var command = new SQLiteCommand(triggerSearchupdate, connection))
{
command.ExecuteNonQuery();
}
string query = @"SELECT COUNT(rowid) as count FROM Beatmap";
using (var command = new SQLiteCommand(query, connection))
{
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
count = reader.GetInt32(reader.GetOrdinal("count"));
}
}
}
}
if (count < osuDatabase.BeatmapCount)
{
DateTime? LastMapInsert = null;
using (var connection = new SQLiteConnection(dburl))
{
connection.Open();
string query = @"SELECT MAX(LastModifiedTime) as Time FROM Beatmap"; ;
using (var command = new SQLiteCommand(query, connection))
{
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
try
{
LastMapInsert = reader.GetDateTime(reader.GetOrdinal("Time"));
}
catch (Exception e)
{
LastMapInsert = null;
}
}
}
}
int i = 0;
int size = osuDatabase.BeatmapCount;
Console.CursorVisible = false;
foreach (var item in osuDatabase.Beatmaps)
{
if (LastMapInsert == null || item.LastModifiedTime > LastMapInsert)
{
insertBeatmap(item);
i++;
Console.CursorTop -= 1;
Console.Write($"Inserted {i}/{size}");
}
}
}
}
}
public static List<Song> GetByRecent(int limit, int offset)
{
var songs = new List<Song>();
using (var connection = new SQLiteConnection(dburl))
{
connection.Open();
string query = @"
SELECT
MD5Hash,
Title,
Artist,
TotalTime,
Creator,
FileName,
FolderName,
AudioFileName
FROM
Beatmap
GROUP BY
BeatmapSetId
ORDER BY
LastModifiedTime DESC
LIMIT @Limit
OFFSET @Offset
";
using (var command = new SQLiteCommand(query, connection))
{
command.Parameters.AddWithValue("@Limit", limit);
command.Parameters.AddWithValue("@Offset", offset);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string folder = reader.GetString(reader.GetOrdinal("FolderName"));
string file = reader.GetString(reader.GetOrdinal("FileName"));
string audio = reader.GetString(reader.GetOrdinal("AudioFileName"));
var img = Osudb.getBG(folder, file);
Song song = new Song(
hash: reader.GetString(reader.GetOrdinal("MD5Hash")),
name: reader.GetString(reader.GetOrdinal("Title")),
artist: reader.GetString(reader.GetOrdinal("Artist")),
length: reader.GetInt32(reader.GetOrdinal("TotalTime")),
url: $"{folder}/{audio}",
previewimage: img,
mapper: reader.GetString(reader.GetOrdinal("Creator"))
);
songs.Add(song);
}
}
}
}
return songs;
}
public void insertBeatmap(OsuParsers.Database.Objects.DbBeatmap beatmap)
{
using (var connection = new SQLiteConnection(dburl))
{
connection.Open();
string insertBeatmap = @"
INSERT INTO Beatmap (
BeatmapId, Artist, ArtistUnicode, Title, TitleUnicode, Creator, Difficulty,
AudioFileName, MD5Hash, FileName, RankedStatus, LastModifiedTime, TotalTime,
AudioPreviewTime, BeatmapSetId, Source, Tags, LastPlayed, FolderName
) VALUES (
@BeatmapId, @Artist, @ArtistUnicode, @Title, @TitleUnicode, @Creator, @Difficulty,
@AudioFileName, @MD5Hash, @FileName, @RankedStatus, @LastModifiedTime, @TotalTime,
@AudioPreviewTime, @BeatmapSetId, @Source, @Tags, @LastPlayed, @FolderName
);";
using (var command = new SQLiteCommand(insertBeatmap, connection))
{
command.Parameters.AddWithValue("@BeatmapSetId", beatmap.BeatmapSetId);
command.Parameters.AddWithValue("@BeatmapId", beatmap.BeatmapId);
command.Parameters.AddWithValue("@Artist", beatmap.Artist);
command.Parameters.AddWithValue("@ArtistUnicode", beatmap.ArtistUnicode);
command.Parameters.AddWithValue("@Title", beatmap.Title);
command.Parameters.AddWithValue("@TitleUnicode", beatmap.TitleUnicode);
command.Parameters.AddWithValue("@Creator", beatmap.Creator);
command.Parameters.AddWithValue("@Difficulty", beatmap.Difficulty);
command.Parameters.AddWithValue("@AudioFileName", beatmap.AudioFileName);
command.Parameters.AddWithValue("@MD5Hash", beatmap.MD5Hash);
command.Parameters.AddWithValue("@FileName", beatmap.FileName);
command.Parameters.AddWithValue("@RankedStatus", beatmap.RankedStatus);
command.Parameters.AddWithValue("@LastModifiedTime", beatmap.LastModifiedTime);
command.Parameters.AddWithValue("@TotalTime", beatmap.TotalTime);
command.Parameters.AddWithValue("@AudioPreviewTime", beatmap.AudioPreviewTime);
command.Parameters.AddWithValue("@Source", beatmap.Source);
command.Parameters.AddWithValue("@Tags", beatmap.Tags);
command.Parameters.AddWithValue("@LastPlayed", beatmap.LastPlayed);
command.Parameters.AddWithValue("@FolderName", beatmap.FolderName);
int rows = command.ExecuteNonQuery();
Console.WriteLine(rows);
}
}
}
public static Song GetSongByHash(string hash)
{
using (var connection = new SQLiteConnection(dburl))
{
connection.Open();
string query = @"
SELECT
MD5Hash,
Title,
Artist,
TotalTime,
Creator,
FileName,
FolderName,
AudioFileName
FROM Beatmap
WHERE MD5Hash = @Hash;
";
using (var command = new SQLiteCommand(query, connection))
{
command.Parameters.AddWithValue("@Hash", hash);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string folder = reader.GetString(reader.GetOrdinal("FolderName"));
string file = reader.GetString(reader.GetOrdinal("FileName"));
string audio = reader.GetString(reader.GetOrdinal("AudioFileName"));
var img = Osudb.getBG(folder, file);
Song song = new Song(
hash: reader.GetString(reader.GetOrdinal("MD5Hash")),
name: reader.GetString(reader.GetOrdinal("Title")),
artist: reader.GetString(reader.GetOrdinal("Artist")),
length: reader.GetInt32(reader.GetOrdinal("TotalTime")),
url: $"{folder}/{audio}",
previewimage: img,
mapper: reader.GetString(reader.GetOrdinal("Creator"))
);
return song;
}
}
}
}
return null;
}
public static ActiveSearch activeSearch(string query) {
ActiveSearch search = new ActiveSearch();
using (var connection = new SQLiteConnection(dburl))
{
string q = @"SELECT
MD5Hash,
Title,
Artist,
TotalTime,
Creator,
FileName,
FolderName,
AudioFileName
FROM Beatmap
WHERE Title LIKE @query
OR Artist LIKE @query
OR Tags LIKE @query
Group By Title
LIMIT 15";
connection.Open();
using (var command = new SQLiteCommand(q, connection))
{
command.Parameters.AddWithValue("@query", "%" + query + "%");
using (var reader = command.ExecuteReader()) {
while (reader.Read()) {
string folder = reader.GetString(reader.GetOrdinal("FolderName"));
string file = reader.GetString(reader.GetOrdinal("FileName"));
string audio = reader.GetString(reader.GetOrdinal("AudioFileName"));
var img = Osudb.getBG(folder, file);
Song song = new Song(
hash: reader.GetString(reader.GetOrdinal("MD5Hash")),
name: reader.GetString(reader.GetOrdinal("Title")),
artist: reader.GetString(reader.GetOrdinal("Artist")),
length: reader.GetInt32(reader.GetOrdinal("TotalTime")),
url: $"{folder}/{audio}",
previewimage: img,
mapper: reader.GetString(reader.GetOrdinal("Creator"))
);
search.Songs.Add(song);
}
}
}
string q2 = @"SELECT
Artist
FROM Beatmap
WHERE Artist LIKE @query
Group By Artist
LIMIT 5";
using (var command = new SQLiteCommand(q2, connection))
{
command.Parameters.AddWithValue("@query", query + "%");
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
search.Artist.Add(reader.GetString(reader.GetOrdinal("Artist")));
}
}
}
}
return search;
}
public static List<Song> GetArtistSearch(string query, int limit, int offset) {
List<Song> songs = new List<Song>();
query = $"%{query}%";
using (var connection = new SQLiteConnection(dburl))
{
string q = @"SELECT
MD5Hash,
Title,
Artist,
TotalTime,
Creator,
FileName,
FolderName,
AudioFileName
FROM Beatmap
WHERE Artist LIKE @query
Group By Title
LIMIT @Limit
OFFSET @Offset";
connection.Open();
using (var command = new SQLiteCommand(q, connection))
{
command.Parameters.AddWithValue("@query", query);
command.Parameters.AddWithValue("@Limit", limit);
command.Parameters.AddWithValue("@Offset", offset);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string folder = reader.GetString(reader.GetOrdinal("FolderName"));
string file = reader.GetString(reader.GetOrdinal("FileName"));
string audio = reader.GetString(reader.GetOrdinal("AudioFileName"));
var img = Osudb.getBG(folder, file);
Song song = new Song(
hash: reader.GetString(reader.GetOrdinal("MD5Hash")),
name: reader.GetString(reader.GetOrdinal("Title")),
artist: reader.GetString(reader.GetOrdinal("Artist")),
length: reader.GetInt32(reader.GetOrdinal("TotalTime")),
url: $"{folder}/{audio}",
previewimage: img,
mapper: reader.GetString(reader.GetOrdinal("Creator"))
);
songs.Add(song);
}
}
}
}
return songs;
}
}
}

View File

@@ -4,6 +4,7 @@ using Microsoft.Win32;
using OsuParsers.Beatmaps; using OsuParsers.Beatmaps;
using OsuParsers.Database; using OsuParsers.Database;
using OsuParsers.Database.Objects; using OsuParsers.Database.Objects;
using shitweb;
using System.Collections; using System.Collections;
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -13,7 +14,6 @@ public class Osudb
private static Osudb instance = null; private static Osudb instance = null;
private static readonly object padlock = new object(); private static readonly object padlock = new object();
public static string osufolder { get; private set; } public static string osufolder { get; private set; }
public static OsuDatabase osuDatabase { get; private set; }
public static CollectionDatabase CollectionDb { get; private set; } public static CollectionDatabase CollectionDb { get; private set; }
static Osudb() static Osudb()
@@ -37,6 +37,7 @@ public class Osudb
static void Parse(string filepath) static void Parse(string filepath)
{ {
OsuDatabase osuDatabase = null;
string file = "/osu!.db"; string file = "/osu!.db";
if (File.Exists(filepath + file)) if (File.Exists(filepath + file))
{ {
@@ -45,6 +46,8 @@ public class Osudb
Console.WriteLine($"Parsing {file}"); Console.WriteLine($"Parsing {file}");
osuDatabase = OsuParsers.Decoders.DatabaseDecoder.DecodeOsu($"{filepath}{file}"); osuDatabase = OsuParsers.Decoders.DatabaseDecoder.DecodeOsu($"{filepath}{file}");
Console.WriteLine($"Parsed {file}"); Console.WriteLine($"Parsed {file}");
fileStream.Close();
} }
} }
@@ -58,6 +61,10 @@ public class Osudb
Console.WriteLine($"Parsed {file}"); Console.WriteLine($"Parsed {file}");
} }
} }
SqliteDB.Instance().setup(osuDatabase);
osuDatabase = null;
GC.Collect();
} }
public static Osudb Instance public static Osudb Instance
@@ -75,15 +82,6 @@ public class Osudb
} }
} }
public DbBeatmap GetBeatmapbyHash(string Hash)
{
if (Hash == null || osuDatabase == null)
{
return null;
}
return osuDatabase.Beatmaps.FirstOrDefault(beatmap => beatmap.MD5Hash == Hash);
}
public static OsuParsers.Database.Objects.Collection GetCollectionbyName(string name) { public static OsuParsers.Database.Objects.Collection GetCollectionbyName(string name) {
return CollectionDb.Collections.FirstOrDefault(collection => collection.Name == name); return CollectionDb.Collections.FirstOrDefault(collection => collection.Name == name);
@@ -101,87 +99,37 @@ public class Osudb
if (collection == null) { return null; } if (collection == null) { return null; }
List<Song> songs = new List<Song>(); List<Song> songs = new List<Song>();
var activeId = 0; var activeId = "";
collection.MD5Hashes.ForEach(hash => collection.MD5Hashes.ForEach(hash =>
{ {
var beatmap = GetBeatmapbyHash(hash); var beatmap = SqliteDB.GetSongByHash(hash);
if (beatmap == null) { return; } if (beatmap == null) { return; }
if (activeId == beatmap.BeatmapSetId) { return; } songs.Add(beatmap);
activeId = beatmap.BeatmapSetId;
//todo
string img = getBG(beatmap.FolderName, beatmap.FileName);
songs.Add(new Song(hash: beatmap.MD5Hash, name: beatmap.Title, artist: beatmap.Artist, length: beatmap.TotalTime, url: $"{beatmap.FolderName}/{beatmap.AudioFileName}" , previewimage: img, mapper: beatmap.Creator));
}); });
return new Collection(collection.Name, songs.Count, songs); return new Collection(collection.Name, songs.Count, songs);
} }
public List<CollectionPreview> GetCollections() public List<CollectionPreview> GetCollections(int limit, int offset)
{ {
List<CollectionPreview> collections = new List<CollectionPreview>(); List<CollectionPreview> collections = new List<CollectionPreview>();
for (int i = 0; i < CollectionDb.Collections.Count; i++) { for (int i = offset; i < CollectionDb.Collections.Count - 1 && i < offset + limit; i++) {
var collection = CollectionDb.Collections[i]; var collection = CollectionDb.Collections[i];
var beatmap = GetBeatmapbyHash(collection.MD5Hashes.FirstOrDefault()); var beatmap = SqliteDB.GetSongByHash(collection.MD5Hashes.FirstOrDefault());
//todo collections.Add(new CollectionPreview(index: i, name: collection.Name, previewimage: beatmap.previewimage, length: collection.Count));
string img = getBG(beatmap.FolderName, beatmap.FileName);
collections.Add(new CollectionPreview(index: i, name: collection.Name, previewimage: img, length: collection.Count));
}; };
return collections; return collections;
} }
public List<Song> GetRecent(int limit, int offset) public static string getBG(string songfolder, string diff)
{
var recent = new List<Song>();
if(limit > 100 && limit < 0) {
limit = 100;
}
var size = osuDatabase.Beatmaps.Count -1;
for (int i = size - offset; i > size - offset - limit; i--)
{
var beatmap = osuDatabase.Beatmaps.ElementAt(i);
if (beatmap == null) {
continue;
}
string img = getBG(beatmap.FolderName, beatmap.FileName);
recent.Add(new Song(
name: beatmap.FileName,
hash: beatmap.MD5Hash,
artist: beatmap.Artist,
length: beatmap.TotalTime,
url: $"{beatmap.FolderName}/{beatmap.AudioFileName}",
previewimage: img,
mapper: beatmap.Creator));
}
return recent;
}
public List<Song> GetFavorites()
{
var recent = new List<Song>();
/*
osuDatabase.Beatmaps.ForEach(beatmap =>
{
Console.WriteLine(beatmap.LastModifiedTime);
});
*/
return null;
}
private static string getBG(string songfolder, string diff)
{ {
string folderpath = Path.Combine(songfolder, diff); string folderpath = Path.Combine(songfolder, diff);
string filepath = Path.Combine(osufolder, "Songs", folderpath); string filepath = Path.Combine(osufolder, "Songs", folderpath);

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="OsuParsers" Version="1.7.1" /> <PackageReference Include="OsuParsers" Version="1.7.1" />
<PackageReference Include="System.Data.SQLite" Version="1.0.118" />
<PackageReference Include="System.Drawing.Common" Version="8.0.8" /> <PackageReference Include="System.Drawing.Common" Version="8.0.8" />
</ItemGroup> </ItemGroup>

View File

@@ -1,3 +1,7 @@
using OsuParsers.Database.Objects;
using OsuParsers.Enums.Database;
using OsuParsers.Enums;
public class Song{ public class Song{
public string hash {get; set;} public string hash {get; set;}
public string name {get; set;} public string name {get; set;}
@@ -37,4 +41,46 @@ public class Collection{
this.name = name; this.length = length; this.songs = songs; this.name = name; this.length = length; this.songs = songs;
} }
}
public class ActiveSearch{
public List<string> Artist { get; set; } = new List<string>();
public List<Song> Songs { get; set; } = new List<Song>();
}
public class Beatmap
{
public string Artist { get; set; }
public string ArtistUnicode { get; set; }
public string Title { get; set; }
public string TitleUnicode { get; set; }
public string Creator { get; set; }
public string Difficulty { get; set; }
public string AudioFileName { get; set; }
public string MD5Hash { get; set; }
public string FileName { get; set; }
public RankedStatus RankedStatus { get; set; }
public DateTime LastModifiedTime { get; set; }
public int TotalTime { get; set; }
public int AudioPreviewTime { get; set; }
public int BeatmapId { get; set; }
public int BeatmapSetId { get; set; }
public string Source { get; set; }
public string Tags { get; set; }
public DateTime LastPlayed { get; set; }
public string FolderName { get; set; }
}
public class BeatmapSet {
public int BeatmapSetId { get; set; }
public string FolderName { get; set; }
public string Creator { get; set; }
public DateTime LastModifiedTime { get; set; }
public List<Beatmap> Beatmaps { get; private set; } = new List<Beatmap>();
public void AddBeatmap(Beatmap beatmap)
{
beatmap.BeatmapSetId = this.BeatmapSetId;
Beatmaps.Add(beatmap);
}
} }

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { updateLanguageServiceSourceFile } from 'typescript';
import type { Song, CollectionPreview } from '../script/types'
import { useAudioStore } from '@/stores/audioStore';
import { ref } from 'vue';
import { RouterLink } from 'vue-router';
const audioStore = useAudioStore()
const props = defineProps<{
songs :Song[];
artist: string[];
}>();
function update(hash: string){
audioStore.setSong(props.songs.at(props.songs.findIndex(s => s.hash==hash)))
}
</script>
<template>
<div class="h-full overflow-scroll border border-pink-500 rounded-lg bg-gray-800">
<h3>Artists</h3>
<ul>
<li v-for="(artist, index) in props.artist" :key="index">
<RouterLink class="flex" :to="'/search?a=' + artist">{{ artist }}</RouterLink>
</li>
</ul>
<h3>Songs</h3>
<ul>
<li v-for="(song, index) in props.songs" :key="index">
<button @click="update(song.hash)" class="flex">
<img class="h-12 w-12" :src="song.previewimage">
<div class="flex flex-col">
{{ song.name }} - {{ song.artist }}
</div>
</button>
</li>
</ul>
</div>
</template>

View File

@@ -70,6 +70,7 @@ export const useAudioStore = defineStore('audioStore', () => {
audio.pause() audio.pause()
audio.currentTime = 0; audio.currentTime = 0;
audio.play() audio.play()
return;
} }
if (shuffle.value) { if (shuffle.value) {
@@ -77,6 +78,7 @@ export const useAudioStore = defineStore('audioStore', () => {
setSong(activeCollection.value[Math.floor(activeCollection.value.length * Math.random())]) setSong(activeCollection.value[Math.floor(activeCollection.value.length * Math.random())])
audio.play() audio.play()
return;
} }
toggleNext() toggleNext()
@@ -89,7 +91,7 @@ export const useAudioStore = defineStore('audioStore', () => {
var audio = document.getElementById("audio-player") as HTMLAudioElement; var audio = document.getElementById("audio-player") as HTMLAudioElement;
audio.currentTime = Math.round((audioslider.value / 100) * audio.duration) audio.currentTime = Math.round((Number(audioslider.value) / 100) * audio.duration)
} }
function togglePrev() { function togglePrev() {

View File

@@ -60,9 +60,9 @@ export const useUserStore = defineStore('userStore', () => {
} }
async function fetchCollections(): Promise<CollectionPreview[]> { async function fetchCollections(offset: number, limit: number): Promise<CollectionPreview[]> {
const cacheKey = 'collections_cache'; const cacheKey = `collections_cache_${offset}_${limit}`;
const url = `${baseUrl.value}api/v1/collections/`; const url = `${baseUrl.value}api/v1/collections/?offset=${offset}&limit=${limit}`;
return fetchWithCache<CollectionPreview[]>(cacheKey, url); return fetchWithCache<CollectionPreview[]>(cacheKey, url);
} }
@@ -84,7 +84,17 @@ export const useUserStore = defineStore('userStore', () => {
return fetchWithCache<Song[]>(cacheKey, url); return fetchWithCache<Song[]>(cacheKey, url);
} }
async function fetchActiveSearch(query: string): Promise<{}> {
const cacheKey = `collections_activeSearch_${query}`;
const url = `${baseUrl.value}api/v1/search/active?q=${query}`;
return fetchWithCache(cacheKey, url);
}
async function fetchSearchArtist(query: string): Promise<Song[]> {
const cacheKey = `collections_artist_${query}`;
const url = `${baseUrl.value}api/v1/search/artist?q=${query}`;
return fetchWithCache<Song[]>(cacheKey, url);
}
return { fetchSong, fetchCollections, fetchCollection, fetchRecent, fetchFavorites, userId, baseUrl } return { fetchSong, fetchActiveSearch, fetchSearchArtist, fetchCollections, fetchCollection, fetchRecent, fetchFavorites, userId, baseUrl }
}) })

View File

@@ -7,20 +7,47 @@ import CollectionListItem from '../components/CollectionListItem.vue'
const userStore = useUserStore(); const userStore = useUserStore();
const collections = ref<CollectionPreview[]>([]); const collections = ref<CollectionPreview[]>([]);
const limit = ref(10);
const offset = ref(0);
const isLoading = ref(false);
onMounted(async () => { const fetchCollections = async () => {
const data = await userStore.fetchCollections(); if (isLoading.value) return;
isLoading.value = true;
const data = await userStore.fetchCollections(offset.value, limit.value);
data.forEach(song => { data.forEach(song => {
song.previewimage = `${userStore.baseUrl}api/v1/images/${song.previewimage}?h=80&w=80`; song.previewimage = `${userStore.baseUrl}api/v1/images/${song.previewimage}?h=80&w=80`;
}) });
collections.value = data;
collections.value = [...collections.value, ...data];
offset.value += limit.value;
isLoading.value = false;
};
onMounted(async () => {
await fetchCollections();
const container = document.querySelector('.collection-container');
if (container) {
container.addEventListener('scroll', async () => {
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
if (scrollTop + clientHeight >= scrollHeight * 0.9 && !isLoading.value) {
await fetchCollections();
}
});
}
}); });
</script> </script>
<template> <template>
<main class="flex-1 text-center flex flex-col h-full overflow-scroll"> <main class="flex-1 text-center flex flex-col h-full overflow-scroll">
<div class="flex flex-col overflow-scroll"> <div class="flex flex-col overflow-scroll collection-container">
<CollectionListItem v-for="(collection, index) in collections" :key="index" :collection="collection" /> <CollectionListItem v-for="(collection, index) in collections" :key="index" :collection="collection" />
</div> </div>
</main> </main>

View File

@@ -49,7 +49,7 @@ const audioStore = useAudioStore();
<div class="flex flex-col justify-between mb-4"> <div class="flex flex-col justify-between mb-4">
<i @click="audioStore.toggleRepeat" :class="[audioStore.repeat ? 'text-pink-500' : '']" <i @click="audioStore.toggleRepeat" :class="[audioStore.repeat ? 'text-pink-500' : '']"
class="fa-solid fa-repeat"></i> class="fa-solid fa-repeat"></i>
<i @click="this.$router.go(-1);" class="fa-solid fa-arrow-down"></i> <i @click="$router.go(-1);" class="fa-solid fa-arrow-down"></i>
</div> </div>
</div> </div>
<div class="flex"> <div class="flex">

View File

@@ -1,10 +1,70 @@
<script setup lang="ts"> <script setup lang="ts">
import SongItem from '../components/SongItem.vue' import SongItem from '../components/SongItem.vue'
import type { Song, CollectionPreview } from '../script/types'
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/userStore';
import { useRoute } from 'vue-router';
import CollectionListItem from '../components/CollectionListItem.vue'
import { useAudioStore } from '@/stores/audioStore';
const route = useRoute();
const userStore = useUserStore();
const audioStore = useAudioStore();
const songs = ref<Song[]>([]);
const name = ref('name');
const limit = ref(100);
const offset = ref(0);
const isLoading = ref(false);
const fetchRecent = async () => {
if (isLoading.value) return;
isLoading.value = true;
const data = await userStore.fetchRecent(limit.value, offset.value);
data.forEach(song => {
song.previewimage = `${userStore.baseUrl}api/v1/images/${song.previewimage}`;
song.url = `${userStore.baseUrl}api/v1/audio/${song.url}`;
});
offset.value += limit.value;
console.log(data)
songs.value = [...songs.value, ...data];
isLoading.value = false;
audioStore.setCollection(null);
}
onMounted(async () => {
await fetchRecent();
const container = document.querySelector('.song-container');
if (container) {
container.addEventListener('scroll', async () => {
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
if (scrollTop + clientHeight >= scrollHeight * 0.9 && !isLoading.value) {
await fetchRecent();
}
});
}
});
</script> </script>
<template> <template>
<main class="flex-1 flex-col overflow-scroll"> <main class="flex-1 flex-col overflow-scroll">
<div class="flex-1 flex-col h-full overflow-scroll song-container">
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
</div>
</main> </main>
</template> </template>

View File

@@ -1,4 +1,81 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Song, CollectionPreview } from '../script/types'
import { useUserStore } from '@/stores/userStore';
import { onMounted, ref, watch } from 'vue';
import ActiveSearchList from '../components/ActiveSearchList.vue'
import { useRoute, useRouter } from 'vue-router';
import SongItem from '../components/SongItem.vue'
import { useAudioStore } from '@/stores/audioStore';
const router = useRouter();
const route = useRoute();
const audioStore = useAudioStore();
const userStore = useUserStore();
const activesongs = ref<Song[]>([]);
const songs = ref<Song[]>([]);
const artists = ref<string[]>([]);
const showSearch = ref(false);
onMounted(async () => {
await loadartistifexist();
const container = document.querySelector('.search') as HTMLInputElement;
if (container) {
container.addEventListener('input', async (event: Event) => {
showSearch.value = true;
const target = event.target as HTMLInputElement;
if(target.value != undefined && target.value != ""){
const data = await userStore.fetchActiveSearch(target.value)
router.push({ query: {s: target.value } });
data.songs.forEach(song => {
song.previewimage = `${userStore.baseUrl}api/v1/images/${song.previewimage}`;
song.url = `${userStore.baseUrl}api/v1/audio/${song.url}`;
});
activesongs.value = data.songs;
audioStore.setCollection(data.songs)
artists.value = data.artist;
} else {
activesongs.value = [];
artists.value = [];
showSearch.value = false;
}
}
)}
const s = route.query.s as string;
if(s){container.value = s; container.dispatchEvent(new Event('input'))}
});
async function loadartistifexist(){
const query = route.query.a as string;
if (query) {
showSearch.value = false;
const data = await userStore.fetchSearchArtist(query)
console.log(data);
data.forEach(song => {
song.previewimage = `${userStore.baseUrl}api/v1/images/${song.previewimage}`;
song.url = `${userStore.baseUrl}api/v1/audio/${song.url}`;
});
songs.value = data;
}
}
watch(() => route.query.a, async (newQuery) => {
await loadartistifexist();
});
</script> </script>
<template> <template>
@@ -12,8 +89,13 @@
<hr> <hr>
</div> </div>
</header> </header>
<main class="flex-1 flex-col"> <main class="flex flex-col flex-1 flex-col w-full h-full overflow-scroll">
<h1> Search...</h1> <input placeholder="Type to Search..." class="flex-1 max-h-12 search border border-pink-500 accent-pink-800 bg-yellow-300 bg-opacity-20 rounded-lg m-2 p-2" />
<input class="flex-1 border border-pink-500 accent-pink-800 bg-yellow-300 bg-opacity-20 rounded-lg m-2 p-2" /> <div class="relative flex flex-col w-full h-full overflow-scroll">
<div v-if="showSearch" class="absolute w-full text-center search-recommendations z -20">
<ActiveSearchList :songs="activesongs" :artist="artists"/>
</div>
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
</div>
</main> </main>
</template> </template>