using Tesses.WebServer; using Scriban; using Newtonsoft.Json; using System.Data.SQLite; using Dapper; using System.Security.Cryptography; //using YouTubeMusicAPI.Models.Info; using YoutubeExplode; using YoutubeExplode.Videos; //using YouTubeMusicAPI.Models.Search; namespace Tesses.YouTubeDownloader2025Companion { public class Database : IDisposable, IAsyncDisposable { public const long Expires = 86400*7; SQLiteConnection conn; public Database() { conn=new SQLiteConnection("Data Source=data/Config/database.db"); } public void Init() { conn.Execute("CREATE TABLE IF NOT EXISTS sessions (Id INTEGER PRIMARY KEY AUTOINCREMENT, AccountName TEXT, Key TEXT UNIQUE, Expires INTEGER);"); conn.Execute("CREATE TABLE IF NOT EXISTS song_videos (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT UNIQUE, VideoId TEXT UNIQUE, VideoTitle TEXT);"); conn.Execute("CREATE TABLE IF NOT EXISTS songs (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT UNIQUE, SongName TEXT);"); conn.Execute("CREATE TABLE IF NOT EXISTS artists (Id INTEGER PRIMARY KEY AUTOINCREMENT, ArtistId TEXT UNIQUE, ArtistName TEXT, ArtistDisambiguation TEXT);"); conn.Execute("CREATE TABLE IF NOT EXISTS albums (Id INTEGER PRIMARY KEY AUTOINCREMENT, AlbumId TEXT UNIQUE, AlbumName TEXT, AlbumReleaseDate TEXT);"); conn.Execute("CREATE TABLE IF NOT EXISTS album_tracks (Id INTEGER PRIMARY KEY AUTOINCREMENT, AlbumId TEXT, SongId TEXT, Track INTEGER, Length INTEGER, UNIQUE(AlbumId, SongId, Track));"); conn.Execute("CREATE TABLE IF NOT EXISTS album_artists (Id INTEGER PRIMARY KEY AUTOINCREMENT, AlbumId TEXT, ArtistId TEXT, UNIQUE(AlbumId, ArtistId));"); conn.Execute("CREATE TABLE IF NOT EXISTS song_artists (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT, ArtistId TEXT, UNIQUE(SongId, ArtistId));"); conn.Execute("CREATE TABLE IF NOT EXISTS tytd_events (Id INTEGER PRIMARY KEY AUTOINCREMENT, Time INTEGER, Type TEXT, VideoTitle TEXT, VideoId TEXT, ChannelTitle TEXT, ChannelId TEXT);"); } public async Task> GetLogEntriesAsync(int offset=0, int count=20) { return await conn.QueryAsync("SELECT * FROM tytd_events ORDER BY Time DESC LIMIT @Count OFFSET @Offset",new DynamicParameters(new{ Offset = offset, Count = count })); } public async Task GetMediaEntryAsync(string albumId, string songId) { var song = await GetSongAsync(songId); if(song is null) return null; var album = await GetAlbumAsync(albumId); if(album is null) return null; var track = (await conn.QueryAsync("SELECT * FROM album_tracks WHERE SongId = @SongId AND AlbumId = @AlbumId;" ,new DynamicParameters(new{SongId = songId, AlbumId = albumId}))).FirstOrDefault(); if(track is null) return null; MediaEntry ent = new MediaEntry(); ent.TrackIndex = track.Track; ent.AlbumId = album.AlbumId; ent.SongName = song.SongName; ent.AlbumArtist = (await GetAlbumArtistsAsync(track.AlbumId)).Select(e=>e.ArtistName).FirstOrDefault() ?? "Unknown Artist"; ent.AlbumName = album.AlbumName; ent.ContributingArtists.AddRange((await GetContributingArtistsAsync(songId)).Select(e=>e.ArtistName)); return ent; } public async IAsyncEnumerable GetMediaEntriesAsync(string songId) { var song = await GetSongAsync(songId); if(song is null) yield break; List contributingArtists = new List(); contributingArtists.AddRange((await GetContributingArtistsAsync(songId)).Select(e=>e.ArtistName)); foreach(var track_info in await conn.QueryAsync("SELECT * FROM album_tracks WHERE SongId = @SongId;" ,new DynamicParameters(new{SongId = songId}))) { MediaEntry ent=new MediaEntry(); ent.TrackIndex = track_info.Track; ent.AlbumId = track_info.AlbumId; ent.SongName = song.SongName; ent.ContributingArtists.AddRange(contributingArtists); ent.AlbumArtist = (await GetAlbumArtistsAsync(track_info.AlbumId)).Select(e=>e.ArtistName).FirstOrDefault() ?? "Unknown Artist"; var album = await GetAlbumAsync(track_info.AlbumId); if(album is not null) { ent.AlbumName = album.AlbumName; } else ent.AlbumName = "Unknown Album"; yield return ent; } } public async Task CreateSessionTokenAsync(string username) { Session session=new Session(); session.AccountName = username; session.Expires = Program.NowEpoch + Expires; session.Key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Expires) VALUES (@AccountName,@Key,@Expires);",session); return session; } public async Task GetVideoIdAsync(string id) { var video = await GetSongVideoFromSongIdAsync(id); if(video is not null) { return VideoId.TryParse(video.VideoId); } return null; } public async Task GetSongIdFromVideoId(VideoId id) { var video = await GetSongVideoFromVideoIdAsync(id); if(video is not null) { return video.SongId; } return null; } public async Task AddVideoIdAsync(string songId, VideoId id, string title) { try { await conn.ExecuteAsync($"INSERT INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new{SongId=songId,VideoId = id.Value, VideoTitle = title})); }catch(Exception){} } public async Task UpdateVideoIdAsync(string songId, VideoId id, string title) { try { await conn.ExecuteAsync($"INSERT OR REPLACE INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new{SongId=songId,VideoId = id.Value, VideoTitle = title})); }catch(Exception){} } public async Task AddAlbumArtistAsync(string albumId, string artistId) { try { await conn.ExecuteAsync($"INSERT INTO album_artists (AlbumId, ArtistId) VALUES (@AlbumId,@ArtistId);", new DynamicParameters(new{AlbumId=albumId,ArtistId = artistId})); }catch(Exception){} } public async Task AddContributingArtistAsync(string songId, string artistId) { try { await conn.ExecuteAsync($"INSERT INTO song_artists (SongId, ArtistId) VALUES (@SongId,@ArtistId);", new DynamicParameters(new{SongId=songId,ArtistId = artistId})); }catch(Exception){} } public async Task GetSongVideoFromVideoIdAsync(VideoId id) { return (await conn.QueryAsync($"SELECT * FROM song_videos WHERE VideoId = @VideoId;", new DynamicParameters(new{VideoId = id.Value}))).FirstOrDefault(); } public async Task GetSongVideoFromSongIdAsync(string id) { return (await conn.QueryAsync($"SELECT * FROM song_videos WHERE SongId = @SongId;", new DynamicParameters(new{SongId = id}))).FirstOrDefault(); } public async Task AddArtistAsync(Artist artist) { try { await conn.ExecuteAsync("INSERT INTO artists (ArtistId,ArtistName,ArtistDisambiguation) VALUES (@ArtistId,@ArtistName,@ArtistDisambiguation);",artist); }catch(Exception) {} } public async Task AddAlbumAsync(Album album) { try { await conn.ExecuteAsync("INSERT INTO albums (AlbumId,AlbumName,AlbumReleaseDate) VALUES (@AlbumId,@AlbumName,@AlbumReleaseDate);",album); }catch(Exception) {} } public async Task AddSongAsync(Song song) { try { await conn.ExecuteAsync("INSERT INTO songs (SongId,SongName) VALUES (@SongId,@SongName);",song); }catch(Exception) {} } public async Task AddTrackAsync(AlbumTrack track) { try { await conn.ExecuteAsync("INSERT INTO album_tracks (AlbumId,SongId,Track,Length) VALUES (@AlbumId,@SongId,@Track,@Length);",track); }catch(Exception) {} } public async Task GetSongAsync(string id) { return (await conn.QueryAsync($"SELECT * FROM songs WHERE SongId = @SongId;", new DynamicParameters(new{SongId = id}))).FirstOrDefault(); } public async Task> GetContributingArtistsAsync(string songId) { return await conn.QueryAsync($"SELECT * FROM song_artists s INNER JOIN artists a ON s.ArtistId = a.ArtistId WHERE s.SongId = @SongId ORDER BY s.Id ASC;", new DynamicParameters(new{SongId = songId})); } public async Task> GetArtistsAsync(string q="", long offset=0, long count=20) { return await conn.QueryAsync("SELECT * FROM artists WHERE ArtistName LIKE @Query LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ Query = $"%{q}%", Offset = offset, Count = count })); } public async Task> GetSongsAsync(string q="", long offset=0, long count=20) { return await conn.QueryAsync("SELECT * FROM songs WHERE SongName LIKE @Query LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ Query = $"%{q}%", Offset = offset, Count = count })); } public async Task> GetAlbumsFromArtistAsync(string artistId, string q = "",long offset=0, long count=20) { return await conn.QueryAsync("SELECT * FROM albums al INNER JOIN album_artists ar ON al.AlbumId = ar.AlbumId WHERE AlbumName LIKE @Query AND ar.ArtistId = @ArtistId ORDER BY al.AlbumReleaseDate, al.Id ASC LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ Query = $"%{q}%", Offset = offset, Count = count, ArtistId = artistId })); } public async Task> GetAlbumsAsync(string q="", long offset=0, long count=20) { return await conn.QueryAsync("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ Query = $"%{q}%", Offset = offset, Count = count })); } public async Task> GetSongVideosAsync() { return await conn.QueryAsync($"SELECT * FROM song_videos;"); } public async Task> GetAlbumArtistsAsync(string albumId) { return await conn.QueryAsync($"SELECT * FROM album_artists s INNER JOIN artists a ON s.ArtistId = a.ArtistId WHERE s.AlbumId = @AlbumId;", new DynamicParameters(new{AlbumId = albumId})); } public async Task GetArtistAsync(string id) { return (await conn.QueryAsync($"SELECT * FROM artists WHERE ArtistId = @ArtistId;", new DynamicParameters(new{ArtistId = id}))).FirstOrDefault(); } public async Task GetAlbumAsync(string id) { return (await conn.QueryAsync("SELECT * FROM albums WHERE AlbumId = @AlbumId;", new DynamicParameters(new{AlbumId = id}))).FirstOrDefault(); } public async Task GetTrackAsync(string songId, string albumId) { return (await conn.QueryAsync("SELECT * FROM album_tracks WHERE AlbumId = @AlbumId AND SongId = @SongId", new DynamicParameters(new{ SongId = songId, AlbumId = albumId}))).FirstOrDefault(); } public async Task> GetTracksAsync(string albumId) { return await conn.QueryAsync("SELECT * FROM songs s INNER JOIN album_tracks t ON s.SongId = t.SongId WHERE t.AlbumId = @AlbumId ORDER BY t.Track ASC;",new DynamicParameters(new{AlbumId = albumId})); } public async Task GetSessionAsync(ServerContext ctx,string sessionToken) { var time = Program.NowEpoch; var res = (await conn.QueryAsync("SELECT * FROM sessions WHERE Key = @Key;", new DynamicParameters(new{Key=sessionToken}))).FirstOrDefault(); if(res is not null) { var expires = res.Expires; if(expires != 0 && time < expires && (expires - time) < (Expires - 3600)) { long expiry = time + Expires; res.Expires = expiry; await conn.ExecuteAsync("UPDATE sessions SET Expires = @Expires WHERE Key = @Key;",res); ctx.ResponseHeaders["Set-Cookie"]=[$"Session={res.Key}; SameSite=Lax; Expires={DateTimeOffset.FromUnixTimeSeconds(expiry).ToString("R")}; HttpOnly"]; } else if(expires != 0 && time >= expires) { await conn.ExecuteAsync("DELETE FROM sessions WHERE Key = @Key;", res); return null; } } return res; } public void Dispose() { conn.Dispose(); } public async ValueTask DisposeAsync() { await conn.DisposeAsync(); } public async Task AddEventAsync(long epoch, Webhook webhook) { //conn.Execute("CREATE TABLE IF NOT EXISTS tytd_events (Id INTEGER PRIMARY KEY AUTOINCREMENT, Time INTEGER, Type TEXT, VideoTitle TEXT, VideoId TEXT, ChannelTitle TEXT, ChannelId TEXT);"); await conn.ExecuteAsync("INSERT INTO tytd_events (Time,Type,VideoTitle,VideoId,ChannelTitle,ChannelId) VALUES (@Time,@Type,@VideoTitle,@VideoId,@ChannelTitle,@ChannelId);", new DynamicParameters(new { Time = epoch, Type = webhook.Type, VideoTitle = webhook.Video.Title, VideoId = webhook.Video.VideoId, ChannelTitle = webhook.Video.Channel, ChannelId = webhook.Video.ChannelId })); } public class Session { public long Id {get;set;} public string AccountName {get;set;}=""; public string Key {get;set;}=""; public long Expires {get;set;} } } }