node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
# db
Should only import `models` internal package.
### 🚫 Do not
- Do not define **models** here.

View File

@@ -0,0 +1,59 @@
package db
import (
"errors"
"seanime/internal/database/models"
"gorm.io/gorm/clause"
)
var accountCache *models.Account
func (db *Database) UpsertAccount(acc *models.Account) (*models.Account, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(acc).Error
if err != nil {
db.Logger.Error().Err(err).Msg("Failed to save account in the database")
return nil, err
}
if acc.Username != "" {
accountCache = acc
} else {
accountCache = nil
}
return acc, nil
}
func (db *Database) GetAccount() (*models.Account, error) {
if accountCache != nil {
return accountCache, nil
}
var acc models.Account
err := db.gormdb.Last(&acc).Error
if err != nil {
return nil, err
}
if acc.Username == "" || acc.Token == "" || acc.Viewer == nil {
return nil, errors.New("account not found")
}
accountCache = &acc
return &acc, err
}
// GetAnilistToken retrieves the AniList token from the account or returns an empty string
func (db *Database) GetAnilistToken() string {
acc, err := db.GetAccount()
if err != nil {
return ""
}
return acc.Token
}

View File

@@ -0,0 +1,57 @@
package db
import (
"seanime/internal/database/models"
)
func (db *Database) GetAutoDownloaderItems() ([]*models.AutoDownloaderItem, error) {
var res []*models.AutoDownloaderItem
err := db.gormdb.Find(&res).Error
if err != nil {
return nil, err
}
return res, nil
}
func (db *Database) GetAutoDownloaderItem(id uint) (*models.AutoDownloaderItem, error) {
var res models.AutoDownloaderItem
err := db.gormdb.First(&res, id).Error
if err != nil {
return nil, err
}
return &res, nil
}
func (db *Database) GetAutoDownloaderItemByMediaId(mId int) ([]*models.AutoDownloaderItem, error) {
var res []*models.AutoDownloaderItem
err := db.gormdb.Where("media_id = ?", mId).Find(&res).Error
if err != nil {
return nil, err
}
return res, nil
}
func (db *Database) InsertAutoDownloaderItem(item *models.AutoDownloaderItem) error {
err := db.gormdb.Create(item).Error
if err != nil {
return err
}
return nil
}
func (db *Database) DeleteAutoDownloaderItem(id uint) error {
return db.gormdb.Delete(&models.AutoDownloaderItem{}, id).Error
}
// DeleteDownloadedAutoDownloaderItems will delete all the downloaded queued items from the database.
func (db *Database) DeleteDownloadedAutoDownloaderItems() error {
return db.gormdb.Where("downloaded = ?", true).Delete(&models.AutoDownloaderItem{}).Error
}
func (db *Database) UpdateAutoDownloaderItem(id uint, item *models.AutoDownloaderItem) error {
// Save the data
return db.gormdb.Model(&models.AutoDownloaderItem{}).Where("id = ?", id).Updates(item).Error
}

View File

@@ -0,0 +1,135 @@
package db
import (
"errors"
"gorm.io/gorm"
"seanime/internal/database/models"
)
func (db *Database) GetChapterDownloadQueue() ([]*models.ChapterDownloadQueueItem, error) {
var res []*models.ChapterDownloadQueueItem
err := db.gormdb.Find(&res).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to get chapter download queue")
return nil, err
}
return res, nil
}
func (db *Database) GetNextChapterDownloadQueueItem() (*models.ChapterDownloadQueueItem, error) {
var res models.ChapterDownloadQueueItem
err := db.gormdb.Where("status = ?", "not_started").First(&res).Error
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
db.Logger.Error().Err(err).Msg("db: Failed to get next chapter download queue item")
}
return nil, nil
}
return &res, nil
}
func (db *Database) DequeueChapterDownloadQueueItem() (*models.ChapterDownloadQueueItem, error) {
// Pop the first item from the queue
var res models.ChapterDownloadQueueItem
err := db.gormdb.Where("status = ?", "downloading").First(&res).Error
if err != nil {
return nil, err
}
err = db.gormdb.Delete(&res).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to delete chapter download queue item")
return nil, err
}
return &res, nil
}
func (db *Database) InsertChapterDownloadQueueItem(item *models.ChapterDownloadQueueItem) error {
// Check if the item already exists
var existingItem models.ChapterDownloadQueueItem
err := db.gormdb.Where("provider = ? AND media_id = ? AND chapter_id = ?", item.Provider, item.MediaID, item.ChapterID).First(&existingItem).Error
if err == nil {
db.Logger.Debug().Msg("db: Chapter download queue item already exists")
return errors.New("chapter is already in the download queue")
}
if item.ChapterID == "" {
return errors.New("chapter ID is empty")
}
if item.Provider == "" {
return errors.New("provider is empty")
}
if item.MediaID == 0 {
return errors.New("media ID is empty")
}
if item.ChapterNumber == "" {
return errors.New("chapter number is empty")
}
err = db.gormdb.Create(item).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to insert chapter download queue item")
return err
}
return nil
}
func (db *Database) UpdateChapterDownloadQueueItemStatus(provider string, mId int, chapterId string, status string) error {
err := db.gormdb.Model(&models.ChapterDownloadQueueItem{}).
Where("provider = ? AND media_id = ? AND chapter_id = ?", provider, mId, chapterId).
Update("status", status).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to update chapter download queue item status")
return err
}
return nil
}
func (db *Database) GetMediaQueuedChapters(mediaId int) ([]*models.ChapterDownloadQueueItem, error) {
var res []*models.ChapterDownloadQueueItem
err := db.gormdb.Where("media_id = ?", mediaId).Find(&res).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to get media queued chapters")
return nil, err
}
return res, nil
}
func (db *Database) ClearAllChapterDownloadQueueItems() error {
err := db.gormdb.
Where("status = ? OR status = ? OR status = ?", "not_started", "downloading", "errored").
Delete(&models.ChapterDownloadQueueItem{}).
Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to clear all chapter download queue items")
return err
}
return nil
}
func (db *Database) ResetErroredChapterDownloadQueueItems() error {
err := db.gormdb.Model(&models.ChapterDownloadQueueItem{}).
Where("status = ?", "errored").
Update("status", "not_started").Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to reset errored chapter download queue items")
return err
}
return nil
}
func (db *Database) ResetDownloadingChapterDownloadQueueItems() error {
err := db.gormdb.Model(&models.ChapterDownloadQueueItem{}).
Where("status = ?", "downloading").
Update("status", "not_started").Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to reset downloading chapter download queue items")
return err
}
return nil
}

View File

@@ -0,0 +1,102 @@
package db
import (
"fmt"
"log"
"os"
"path/filepath"
"seanime/internal/database/models"
"time"
"github.com/glebarez/sqlite"
"github.com/rs/zerolog"
"github.com/samber/mo"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
type Database struct {
gormdb *gorm.DB
Logger *zerolog.Logger
CurrMediaFillers mo.Option[map[int]*MediaFillerItem]
}
func (db *Database) Gorm() *gorm.DB {
return db.gormdb
}
func NewDatabase(appDataDir, dbName string, logger *zerolog.Logger) (*Database, error) {
// Set the SQLite database path
var sqlitePath string
if os.Getenv("TEST_ENV") == "true" {
sqlitePath = ":memory:"
} else {
sqlitePath = filepath.Join(appDataDir, dbName+".db")
}
// Connect to the SQLite database
db, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{
Logger: gormlogger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
gormlogger.Config{
SlowThreshold: time.Second,
LogLevel: gormlogger.Error,
IgnoreRecordNotFoundError: true,
ParameterizedQueries: false,
Colorful: true,
},
),
})
if err != nil {
return nil, err
}
// Migrate tables
err = migrateTables(db)
if err != nil {
logger.Fatal().Err(err).Msg("db: Failed to perform auto migration")
return nil, err
}
logger.Info().Str("name", fmt.Sprintf("%s.db", dbName)).Msg("db: Database instantiated")
return &Database{
gormdb: db,
Logger: logger,
CurrMediaFillers: mo.None[map[int]*MediaFillerItem](),
}, nil
}
// MigrateTables performs auto migration on the database
func migrateTables(db *gorm.DB) error {
err := db.AutoMigrate(
&models.LocalFiles{},
&models.Settings{},
&models.Account{},
&models.Mal{},
&models.ScanSummary{},
&models.AutoDownloaderRule{},
&models.AutoDownloaderItem{},
&models.SilencedMediaEntry{},
&models.Theme{},
&models.PlaylistEntry{},
&models.ChapterDownloadQueueItem{},
&models.TorrentstreamSettings{},
&models.TorrentstreamHistory{},
&models.MediastreamSettings{},
&models.MediaFiller{},
&models.MangaMapping{},
&models.OnlinestreamMapping{},
&models.DebridSettings{},
&models.DebridTorrentItem{},
&models.PluginData{},
//&models.MangaChapterContainer{},
)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,56 @@
package db
import (
"seanime/internal/database/models"
)
func (db *Database) GetDebridTorrentItems() ([]*models.DebridTorrentItem, error) {
var res []*models.DebridTorrentItem
err := db.gormdb.Find(&res).Error
if err != nil {
return nil, err
}
return res, nil
}
func (db *Database) GetDebridTorrentItemByDbId(dbId uint) (*models.DebridTorrentItem, error) {
var res models.DebridTorrentItem
err := db.gormdb.First(&res, dbId).Error
if err != nil {
return nil, err
}
return &res, nil
}
func (db *Database) GetDebridTorrentItemByTorrentItemId(tId string) (*models.DebridTorrentItem, error) {
var res *models.DebridTorrentItem
err := db.gormdb.Where("torrent_item_id = ?", tId).First(&res).Error
if err != nil {
return nil, err
}
return res, nil
}
func (db *Database) InsertDebridTorrentItem(item *models.DebridTorrentItem) error {
err := db.gormdb.Create(item).Error
if err != nil {
return err
}
return nil
}
func (db *Database) DeleteDebridTorrentItemByDbId(dbId uint) error {
return db.gormdb.Delete(&models.DebridTorrentItem{}, dbId).Error
}
func (db *Database) DeleteDebridTorrentItemByTorrentItemId(tId string) error {
return db.gormdb.Where("torrent_item_id = ?", tId).Delete(&models.DebridTorrentItem{}).Error
}
func (db *Database) UpdateDebridTorrentItemByDbId(dbId uint, item *models.DebridTorrentItem) error {
// Save the data
return db.gormdb.Model(&models.DebridTorrentItem{}).Where("id = ?", dbId).Updates(item).Error
}

View File

@@ -0,0 +1,51 @@
package db
import (
"seanime/internal/database/models"
"gorm.io/gorm/clause"
)
// TrimLocalFileEntries will trim the local file entries if there are more than 10 entries.
// This is run in a goroutine.
func (db *Database) TrimLocalFileEntries() {
go func() {
var count int64
err := db.gormdb.Model(&models.LocalFiles{}).Count(&count).Error
if err != nil {
db.Logger.Error().Err(err).Msg("database: Failed to count local file entries")
return
}
if count > 10 {
// Leave 5 entries
err = db.gormdb.Delete(&models.LocalFiles{}, "id IN (SELECT id FROM local_files ORDER BY id ASC LIMIT ?)", count-5).Error
if err != nil {
db.Logger.Error().Err(err).Msg("database: Failed to delete old local file entries")
return
}
}
}()
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (db *Database) UpsertLocalFiles(lfs *models.LocalFiles) (*models.LocalFiles, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(lfs).Error
if err != nil {
return nil, err
}
return lfs, nil
}
func (db *Database) InsertLocalFiles(lfs *models.LocalFiles) (*models.LocalFiles, error) {
err := db.gormdb.Create(lfs).Error
if err != nil {
return nil, err
}
return lfs, nil
}

View File

@@ -0,0 +1,50 @@
package db
import (
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"seanime/internal/database/models"
)
func (db *Database) GetMalInfo() (*models.Mal, error) {
// Get the first entry
var res models.Mal
err := db.gormdb.First(&res, 1).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("MAL not connected")
} else if err != nil {
return nil, err
}
return &res, nil
}
func (db *Database) UpsertMalInfo(info *models.Mal) (*models.Mal, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(info).Error
if err != nil {
return nil, err
}
return info, nil
}
func (db *Database) InsertMalInfo(info *models.Mal) (*models.Mal, error) {
err := db.gormdb.Create(info).Error
if err != nil {
return nil, err
}
return info, nil
}
func (db *Database) DeleteMalInfo() error {
err := db.gormdb.Delete(&models.Mal{}, 1).Error
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,102 @@
package db
import (
"fmt"
"seanime/internal/database/models"
"seanime/internal/util/result"
)
var mangaMappingCache = result.NewResultMap[string, *models.MangaMapping]()
func formatMangaMappingCacheKey(provider string, mediaId int) string {
return fmt.Sprintf("%s$%d", provider, mediaId)
}
func (db *Database) GetMangaMapping(provider string, mediaId int) (*models.MangaMapping, bool) {
if res, ok := mangaMappingCache.Get(formatMangaMappingCacheKey(provider, mediaId)); ok {
return res, true
}
var res models.MangaMapping
err := db.gormdb.Where("provider = ? AND media_id = ?", provider, mediaId).First(&res).Error
if err != nil {
return nil, false
}
mangaMappingCache.Set(formatMangaMappingCacheKey(provider, mediaId), &res)
return &res, true
}
func (db *Database) InsertMangaMapping(provider string, mediaId int, mangaId string) error {
mapping := models.MangaMapping{
Provider: provider,
MediaID: mediaId,
MangaID: mangaId,
}
mangaMappingCache.Set(formatMangaMappingCacheKey(provider, mediaId), &mapping)
return db.gormdb.Save(&mapping).Error
}
func (db *Database) DeleteMangaMapping(provider string, mediaId int) error {
err := db.gormdb.Where("provider = ? AND media_id = ?", provider, mediaId).Delete(&models.MangaMapping{}).Error
if err != nil {
return err
}
mangaMappingCache.Delete(formatMangaMappingCacheKey(provider, mediaId))
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var mangaChapterContainerCache = result.NewResultMap[string, *models.MangaChapterContainer]()
func formatMangaChapterContainerCacheKey(provider string, mediaId int, chapterId string) string {
return fmt.Sprintf("%s$%d$%s", provider, mediaId, chapterId)
}
func (db *Database) GetMangaChapterContainer(provider string, mediaId int, chapterId string) (*models.MangaChapterContainer, bool) {
if res, ok := mangaChapterContainerCache.Get(formatMangaChapterContainerCacheKey(provider, mediaId, chapterId)); ok {
return res, true
}
var res models.MangaChapterContainer
err := db.gormdb.Where("provider = ? AND media_id = ? AND chapter_id = ?", provider, mediaId, chapterId).First(&res).Error
if err != nil {
return nil, false
}
mangaChapterContainerCache.Set(formatMangaChapterContainerCacheKey(provider, mediaId, chapterId), &res)
return &res, true
}
func (db *Database) InsertMangaChapterContainer(provider string, mediaId int, chapterId string, chapterContainer []byte) error {
container := models.MangaChapterContainer{
Provider: provider,
MediaID: mediaId,
ChapterID: chapterId,
Data: chapterContainer,
}
mangaChapterContainerCache.Set(formatMangaChapterContainerCacheKey(provider, mediaId, chapterId), &container)
return db.gormdb.Save(&container).Error
}
func (db *Database) DeleteMangaChapterContainer(provider string, mediaId int, chapterId string) error {
err := db.gormdb.Where("provider = ? AND media_id = ? AND chapter_id = ?", provider, mediaId, chapterId).Delete(&models.MangaChapterContainer{}).Error
if err != nil {
return err
}
mangaChapterContainerCache.Delete(formatMangaChapterContainerCacheKey(provider, mediaId, chapterId))
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,182 @@
package db
import (
"github.com/goccy/go-json"
"github.com/samber/mo"
"seanime/internal/api/filler"
"seanime/internal/database/models"
"time"
)
type MediaFillerItem struct {
DbId uint `json:"dbId"`
Provider string `json:"provider"`
Slug string `json:"slug"`
MediaId int `json:"mediaId"`
LastFetchedAt time.Time `json:"lastFetchedAt"`
FillerEpisodes []string `json:"fillerEpisodes"`
}
// GetCachedMediaFillers will return all the media fillers (cache-first).
// If the cache is empty, it will fetch the media fillers from the database.
func (db *Database) GetCachedMediaFillers() (map[int]*MediaFillerItem, error) {
if db.CurrMediaFillers.IsPresent() {
return db.CurrMediaFillers.MustGet(), nil
}
var res []*models.MediaFiller
err := db.gormdb.Find(&res).Error
if err != nil {
return nil, err
}
// Unmarshal the media fillers
mediaFillers := make(map[int]*MediaFillerItem)
for _, mf := range res {
var fillerData filler.Data
if err := json.Unmarshal(mf.Data, &fillerData); err != nil {
return nil, err
}
// Get the filler episodes
var fillerEpisodes []string
if fillerData.FillerEpisodes != nil || len(fillerData.FillerEpisodes) > 0 {
fillerEpisodes = fillerData.FillerEpisodes
}
mediaFillers[mf.MediaID] = &MediaFillerItem{
DbId: mf.ID,
Provider: mf.Provider,
MediaId: mf.MediaID,
Slug: mf.Slug,
LastFetchedAt: mf.LastFetchedAt,
FillerEpisodes: fillerEpisodes,
}
}
// Cache the media fillers
db.CurrMediaFillers = mo.Some(mediaFillers)
return db.CurrMediaFillers.MustGet(), nil
}
func (db *Database) GetMediaFillerItem(mediaId int) (*MediaFillerItem, bool) {
mediaFillers, err := db.GetCachedMediaFillers()
if err != nil {
return nil, false
}
item, ok := mediaFillers[mediaId]
return item, ok
}
func (db *Database) InsertMediaFiller(
provider string,
mediaId int,
slug string,
lastFetchedAt time.Time,
fillerEpisodes []string,
) error {
// Marshal the filler data
fillerData := filler.Data{
FillerEpisodes: fillerEpisodes,
}
fillerDataBytes, err := json.Marshal(fillerData)
if err != nil {
return err
}
// Delete the existing media filler
_ = db.DeleteMediaFiller(mediaId)
// Save the media filler
err = db.gormdb.Create(&models.MediaFiller{
Provider: provider,
MediaID: mediaId,
Slug: slug,
LastFetchedAt: lastFetchedAt,
Data: fillerDataBytes,
}).Error
if err != nil {
return err
}
// Update the cache
db.CurrMediaFillers = mo.None[map[int]*MediaFillerItem]()
return nil
}
// SaveCachedMediaFillerItems will save the cached media filler items in the database.
// Call this function after editing the cached media filler items.
func (db *Database) SaveCachedMediaFillerItems() error {
if db.CurrMediaFillers.IsAbsent() {
return nil
}
mediaFillers, err := db.GetCachedMediaFillers()
if err != nil {
return err
}
for _, mf := range mediaFillers {
if len(mf.FillerEpisodes) == 0 {
continue
}
// Marshal the filler data
fillerData := filler.Data{
FillerEpisodes: mf.FillerEpisodes,
}
fillerDataBytes, err := json.Marshal(fillerData)
if err != nil {
return err
}
// Save the media filler
err = db.gormdb.Model(&models.MediaFiller{}).
Where("id = ?", mf.DbId).
Updates(map[string]interface{}{
"last_fetched_at": mf.LastFetchedAt,
"data": fillerDataBytes,
}).Error
if err != nil {
return err
}
}
// Update the cache
db.CurrMediaFillers = mo.None[map[int]*MediaFillerItem]()
return nil
}
func (db *Database) DeleteMediaFiller(mediaId int) error {
mediaFillers, err := db.GetCachedMediaFillers()
if err != nil {
return err
}
item, ok := mediaFillers[mediaId]
if !ok {
return nil
}
err = db.gormdb.Delete(&models.MediaFiller{}, item.DbId).Error
if err != nil {
return err
}
// Update the cache
db.CurrMediaFillers = mo.None[map[int]*MediaFillerItem]()
return nil
}

View File

@@ -0,0 +1,24 @@
package db
import (
"seanime/internal/database/models"
)
func (db *Database) UpsertNakamaSettings(nakamaSettings *models.NakamaSettings) (*models.NakamaSettings, error) {
// Get current settings
currentSettings, err := db.GetSettings()
if err != nil {
return nil, err
}
// Update the settings
*(currentSettings.Nakama) = *nakamaSettings
_, err = db.UpsertSettings(currentSettings)
if err != nil {
return nil, err
}
return nakamaSettings, nil
}

View File

@@ -0,0 +1,52 @@
package db
import (
"fmt"
"seanime/internal/database/models"
"seanime/internal/util/result"
)
var onlinestreamMappingCache = result.NewResultMap[string, *models.OnlinestreamMapping]()
func formatOnlinestreamMappingCacheKey(provider string, mediaId int) string {
return fmt.Sprintf("%s$%d", provider, mediaId)
}
func (db *Database) GetOnlinestreamMapping(provider string, mediaId int) (*models.OnlinestreamMapping, bool) {
if res, ok := onlinestreamMappingCache.Get(formatOnlinestreamMappingCacheKey(provider, mediaId)); ok {
return res, true
}
var res models.OnlinestreamMapping
err := db.gormdb.Where("provider = ? AND media_id = ?", provider, mediaId).First(&res).Error
if err != nil {
return nil, false
}
onlinestreamMappingCache.Set(formatOnlinestreamMappingCacheKey(provider, mediaId), &res)
return &res, true
}
func (db *Database) InsertOnlinestreamMapping(provider string, mediaId int, animeId string) error {
mapping := models.OnlinestreamMapping{
Provider: provider,
MediaID: mediaId,
AnimeID: animeId,
}
onlinestreamMappingCache.Set(formatOnlinestreamMappingCacheKey(provider, mediaId), &mapping)
return db.gormdb.Save(&mapping).Error
}
func (db *Database) DeleteOnlinestreamMapping(provider string, mediaId int) error {
err := db.gormdb.Where("provider = ? AND media_id = ?", provider, mediaId).Delete(&models.OnlinestreamMapping{}).Error
if err != nil {
return err
}
onlinestreamMappingCache.Delete(formatOnlinestreamMappingCacheKey(provider, mediaId))
return nil
}

View File

@@ -0,0 +1,24 @@
package db
import (
"seanime/internal/database/models"
)
func (db *Database) TrimScanSummaryEntries() {
go func() {
var count int64
err := db.gormdb.Model(&models.ScanSummary{}).Count(&count).Error
if err != nil {
db.Logger.Error().Err(err).Msg("Failed to count scan summary entries")
return
}
if count > 10 {
// Leave 5 entries
err = db.gormdb.Delete(&models.ScanSummary{}, "id IN (SELECT id FROM scan_summaries ORDER BY id ASC LIMIT ?)", count-5).Error
if err != nil {
db.Logger.Error().Err(err).Msg("Failed to delete old scan summary entries")
return
}
}
}()
}

View File

@@ -0,0 +1,200 @@
package db
import (
"seanime/internal/database/models"
"gorm.io/gorm/clause"
)
var CurrSettings *models.Settings
func (db *Database) UpsertSettings(settings *models.Settings) (*models.Settings, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(settings).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to save settings in the database")
return nil, err
}
CurrSettings = settings
db.Logger.Debug().Msg("db: Settings saved")
return settings, nil
}
func (db *Database) GetSettings() (*models.Settings, error) {
if CurrSettings != nil {
return CurrSettings, nil
}
var settings models.Settings
err := db.gormdb.Where("id = ?", 1).Find(&settings).Error
if err != nil {
return nil, err
}
return &settings, nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (db *Database) GetLibraryPathFromSettings() (string, error) {
settings, err := db.GetSettings()
if err != nil {
return "", err
}
return settings.Library.LibraryPath, nil
}
func (db *Database) GetAdditionalLibraryPathsFromSettings() ([]string, error) {
settings, err := db.GetSettings()
if err != nil {
return []string{}, err
}
return settings.Library.LibraryPaths, nil
}
func (db *Database) GetAllLibraryPathsFromSettings() ([]string, error) {
settings, err := db.GetSettings()
if err != nil {
return []string{}, err
}
if settings.Library == nil {
return []string{}, nil
}
return append([]string{settings.Library.LibraryPath}, settings.Library.LibraryPaths...), nil
}
func (db *Database) AllLibraryPathsFromSettings(settings *models.Settings) *[]string {
if settings.Library == nil {
return &[]string{}
}
r := append([]string{settings.Library.LibraryPath}, settings.Library.LibraryPaths...)
return &r
}
func (db *Database) AutoUpdateProgressIsEnabled() (bool, error) {
settings, err := db.GetSettings()
if err != nil {
return false, err
}
return settings.Library.AutoUpdateProgress, nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var CurrMediastreamSettings *models.MediastreamSettings
func (db *Database) UpsertMediastreamSettings(settings *models.MediastreamSettings) (*models.MediastreamSettings, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(settings).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to save media streaming settings in the database")
return nil, err
}
CurrMediastreamSettings = settings
db.Logger.Debug().Msg("db: Media streaming settings saved")
return settings, nil
}
func (db *Database) GetMediastreamSettings() (*models.MediastreamSettings, bool) {
if CurrMediastreamSettings != nil {
return CurrMediastreamSettings, true
}
var settings models.MediastreamSettings
err := db.gormdb.Where("id = ?", 1).First(&settings).Error
if err != nil {
return nil, false
}
return &settings, true
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var CurrTorrentstreamSettings *models.TorrentstreamSettings
func (db *Database) UpsertTorrentstreamSettings(settings *models.TorrentstreamSettings) (*models.TorrentstreamSettings, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(settings).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to save torrent streaming settings in the database")
return nil, err
}
CurrTorrentstreamSettings = settings
db.Logger.Debug().Msg("db: Torrent streaming settings saved")
return settings, nil
}
func (db *Database) GetTorrentstreamSettings() (*models.TorrentstreamSettings, bool) {
if CurrTorrentstreamSettings != nil {
return CurrTorrentstreamSettings, true
}
var settings models.TorrentstreamSettings
err := db.gormdb.Where("id = ?", 1).First(&settings).Error
if err != nil {
return nil, false
}
return &settings, true
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var CurrentDebridSettings *models.DebridSettings
func (db *Database) UpsertDebridSettings(settings *models.DebridSettings) (*models.DebridSettings, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(settings).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to save debrid settings in the database")
return nil, err
}
CurrentDebridSettings = settings
db.Logger.Debug().Msg("db: Debrid settings saved")
return settings, nil
}
func (db *Database) GetDebridSettings() (*models.DebridSettings, bool) {
if CurrentDebridSettings != nil {
return CurrentDebridSettings, true
}
var settings models.DebridSettings
err := db.gormdb.Where("id = ?", 1).First(&settings).Error
if err != nil {
return nil, false
}
return &settings, true
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,70 @@
package db
import (
"gorm.io/gorm/clause"
"seanime/internal/database/models"
)
func (db *Database) GetSilencedMediaEntries() ([]*models.SilencedMediaEntry, error) {
var res []*models.SilencedMediaEntry
err := db.gormdb.Find(&res).Error
if err != nil {
return nil, err
}
return res, nil
}
// GetSilencedMediaEntryIds returns the ids of all silenced media entries.
// It returns an empty slice if there is an error.
func (db *Database) GetSilencedMediaEntryIds() ([]int, error) {
var res []*models.SilencedMediaEntry
err := db.gormdb.Find(&res).Error
if err != nil {
return make([]int, 0), err
}
if len(res) == 0 {
return make([]int, 0), nil
}
mIds := make([]int, len(res))
for i, v := range res {
mIds[i] = int(v.ID)
}
return mIds, nil
}
func (db *Database) GetSilencedMediaEntry(mId uint) (*models.SilencedMediaEntry, error) {
var res models.SilencedMediaEntry
err := db.gormdb.First(&res, mId).Error
if err != nil {
return nil, err
}
return &res, nil
}
func (db *Database) InsertSilencedMediaEntry(mId uint) error {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(&models.SilencedMediaEntry{
BaseModel: models.BaseModel{
ID: mId,
},
}).Error
if err != nil {
return err
}
return nil
}
func (db *Database) DeleteSilencedMediaEntry(id uint) error {
err := db.gormdb.Delete(&models.SilencedMediaEntry{}, id).Error
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,47 @@
package db
import (
"gorm.io/gorm/clause"
"seanime/internal/database/models"
)
var themeCache *models.Theme
func (db *Database) GetTheme() (*models.Theme, error) {
if themeCache != nil {
return themeCache, nil
}
var theme models.Theme
err := db.gormdb.Where("id = ?", 1).Find(&theme).Error
if err != nil {
return nil, err
}
themeCache = &theme
return &theme, nil
}
// UpsertTheme updates the theme settings.
func (db *Database) UpsertTheme(settings *models.Theme) (*models.Theme, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(settings).Error
if err != nil {
db.Logger.Error().Err(err).Msg("db: Failed to save theme in the database")
return nil, err
}
db.Logger.Debug().Msg("db: Theme saved")
themeCache = settings
return settings, nil
}

View File

@@ -0,0 +1,21 @@
package db
import (
"gorm.io/gorm/clause"
"seanime/internal/database/models"
)
func (db *Database) UpsertToken(token *models.Token) (*models.Token, error) {
err := db.gormdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}),
}).Create(token).Error
if err != nil {
db.Logger.Error().Err(err).Msg("Failed to save token in the database")
return nil, err
}
return token, nil
}

View File

@@ -0,0 +1,24 @@
package db
import (
"seanime/internal/database/models"
)
func (db *Database) TrimTorrentstreamHistory() {
go func() {
var count int64
err := db.gormdb.Model(&models.TorrentstreamHistory{}).Count(&count).Error
if err != nil {
db.Logger.Error().Err(err).Msg("database: Failed to count torrent stream history entries")
return
}
if count > 50 {
// Leave 40 entries
err = db.gormdb.Delete(&models.TorrentstreamHistory{}, "id IN (SELECT id FROM torrentstream_histories ORDER BY updated_at ASC LIMIT ?)", 10).Error
if err != nil {
db.Logger.Error().Err(err).Msg("database: Failed to delete old torrent stream history entries")
return
}
}
}()
}

View File

@@ -0,0 +1,2 @@
The database may store some structs defined outside as `[]byte` inside `models`.
To avoid circular dependencies, we define methods that directly convert `[]byte` to the corresponding struct using the database to store/retrieve them.

View File

@@ -0,0 +1,109 @@
package db_bridge
import (
"github.com/goccy/go-json"
"seanime/internal/database/db"
"seanime/internal/database/models"
"seanime/internal/library/anime"
)
var CurrAutoDownloaderRules []*anime.AutoDownloaderRule
func GetAutoDownloaderRules(db *db.Database) ([]*anime.AutoDownloaderRule, error) {
//if CurrAutoDownloaderRules != nil {
// return CurrAutoDownloaderRules, nil
//}
var res []*models.AutoDownloaderRule
err := db.Gorm().Find(&res).Error
if err != nil {
return nil, err
}
// Unmarshal the data
var rules []*anime.AutoDownloaderRule
for _, r := range res {
smBytes := r.Value
var sm anime.AutoDownloaderRule
if err := json.Unmarshal(smBytes, &sm); err != nil {
return nil, err
}
sm.DbID = r.ID
rules = append(rules, &sm)
}
//CurrAutoDownloaderRules = rules
return rules, nil
}
func GetAutoDownloaderRule(db *db.Database, id uint) (*anime.AutoDownloaderRule, error) {
var res models.AutoDownloaderRule
err := db.Gorm().First(&res, id).Error
if err != nil {
return nil, err
}
// Unmarshal the data
smBytes := res.Value
var sm anime.AutoDownloaderRule
if err := json.Unmarshal(smBytes, &sm); err != nil {
return nil, err
}
sm.DbID = res.ID
return &sm, nil
}
func GetAutoDownloaderRulesByMediaId(db *db.Database, mediaId int) (ret []*anime.AutoDownloaderRule) {
rules, err := GetAutoDownloaderRules(db)
if err != nil {
return
}
for _, rule := range rules {
if rule.MediaId == mediaId {
ret = append(ret, rule)
}
}
return
}
func InsertAutoDownloaderRule(db *db.Database, sm *anime.AutoDownloaderRule) error {
CurrAutoDownloaderRules = nil
// Marshal the data
bytes, err := json.Marshal(sm)
if err != nil {
return err
}
// Save the data
return db.Gorm().Create(&models.AutoDownloaderRule{
Value: bytes,
}).Error
}
func DeleteAutoDownloaderRule(db *db.Database, id uint) error {
CurrAutoDownloaderRules = nil
return db.Gorm().Delete(&models.AutoDownloaderRule{}, id).Error
}
func UpdateAutoDownloaderRule(db *db.Database, id uint, sm *anime.AutoDownloaderRule) error {
CurrAutoDownloaderRules = nil
// Marshal the data
bytes, err := json.Marshal(sm)
if err != nil {
return err
}
// Save the data
return db.Gorm().Model(&models.AutoDownloaderRule{}).Where("id = ?", id).Update("value", bytes).Error
}

View File

@@ -0,0 +1,97 @@
package db_bridge
import (
"github.com/goccy/go-json"
"github.com/samber/mo"
"seanime/internal/database/db"
"seanime/internal/database/models"
"seanime/internal/library/anime"
)
var CurrLocalFilesDbId uint
var CurrLocalFiles mo.Option[[]*anime.LocalFile]
// GetLocalFiles will return the latest local files and the id of the entry.
func GetLocalFiles(db *db.Database) ([]*anime.LocalFile, uint, error) {
if CurrLocalFiles.IsPresent() {
return CurrLocalFiles.MustGet(), CurrLocalFilesDbId, nil
}
// Get the latest entry
var res models.LocalFiles
err := db.Gorm().Last(&res).Error
if err != nil {
return nil, 0, err
}
// Unmarshal the local files
lfsBytes := res.Value
var lfs []*anime.LocalFile
if err := json.Unmarshal(lfsBytes, &lfs); err != nil {
return nil, 0, err
}
db.Logger.Debug().Msg("db: Local files retrieved")
CurrLocalFiles = mo.Some(lfs)
CurrLocalFilesDbId = res.ID
return lfs, res.ID, nil
}
// SaveLocalFiles will save the local files in the database at the given id.
func SaveLocalFiles(db *db.Database, lfsId uint, lfs []*anime.LocalFile) ([]*anime.LocalFile, error) {
// Marshal the local files
marshaledLfs, err := json.Marshal(lfs)
if err != nil {
return nil, err
}
// Save the local files
ret, err := db.UpsertLocalFiles(&models.LocalFiles{
BaseModel: models.BaseModel{
ID: lfsId,
},
Value: marshaledLfs,
})
if err != nil {
return nil, err
}
// Unmarshal the saved local files
var retLfs []*anime.LocalFile
if err := json.Unmarshal(ret.Value, &retLfs); err != nil {
return lfs, nil
}
CurrLocalFiles = mo.Some(retLfs)
CurrLocalFilesDbId = ret.ID
return retLfs, nil
}
// InsertLocalFiles will insert the local files in the database at a new entry.
func InsertLocalFiles(db *db.Database, lfs []*anime.LocalFile) ([]*anime.LocalFile, error) {
// Marshal the local files
bytes, err := json.Marshal(lfs)
if err != nil {
return nil, err
}
// Save the local files to the database
ret, err := db.InsertLocalFiles(&models.LocalFiles{
Value: bytes,
})
if err != nil {
return nil, err
}
CurrLocalFiles = mo.Some(lfs)
CurrLocalFilesDbId = ret.ID
return lfs, nil
}

View File

@@ -0,0 +1,82 @@
package db_bridge
import (
"github.com/goccy/go-json"
"seanime/internal/database/db"
"seanime/internal/database/models"
"seanime/internal/library/anime"
)
func GetPlaylists(db *db.Database) ([]*anime.Playlist, error) {
var res []*models.PlaylistEntry
err := db.Gorm().Find(&res).Error
if err != nil {
return nil, err
}
playlists := make([]*anime.Playlist, 0)
for _, p := range res {
var localFiles []*anime.LocalFile
if err := json.Unmarshal(p.Value, &localFiles); err == nil {
playlist := anime.NewPlaylist(p.Name)
playlist.SetLocalFiles(localFiles)
playlist.DbId = p.ID
playlists = append(playlists, playlist)
}
}
return playlists, nil
}
func SavePlaylist(db *db.Database, playlist *anime.Playlist) error {
data, err := json.Marshal(playlist.LocalFiles)
if err != nil {
return err
}
playlistEntry := &models.PlaylistEntry{
Name: playlist.Name,
Value: data,
}
return db.Gorm().Save(playlistEntry).Error
}
func DeletePlaylist(db *db.Database, id uint) error {
return db.Gorm().Where("id = ?", id).Delete(&models.PlaylistEntry{}).Error
}
func UpdatePlaylist(db *db.Database, playlist *anime.Playlist) error {
data, err := json.Marshal(playlist.LocalFiles)
if err != nil {
return err
}
// Get the playlist entry
playlistEntry := &models.PlaylistEntry{}
if err := db.Gorm().Where("id = ?", playlist.DbId).First(playlistEntry).Error; err != nil {
return err
}
// Update the playlist entry
playlistEntry.Name = playlist.Name
playlistEntry.Value = data
return db.Gorm().Save(playlistEntry).Error
}
func GetPlaylist(db *db.Database, id uint) (*anime.Playlist, error) {
playlistEntry := &models.PlaylistEntry{}
if err := db.Gorm().Where("id = ?", id).First(playlistEntry).Error; err != nil {
return nil, err
}
var localFiles []*anime.LocalFile
if err := json.Unmarshal(playlistEntry.Value, &localFiles); err != nil {
return nil, err
}
playlist := anime.NewPlaylist(playlistEntry.Name)
playlist.SetLocalFiles(localFiles)
playlist.DbId = playlistEntry.ID
return playlist, nil
}

View File

@@ -0,0 +1,50 @@
package db_bridge
import (
"seanime/internal/database/db"
"seanime/internal/database/models"
"seanime/internal/library/summary"
"github.com/goccy/go-json"
)
func GetScanSummaries(database *db.Database) ([]*summary.ScanSummaryItem, error) {
var res []*models.ScanSummary
err := database.Gorm().Find(&res).Error
if err != nil {
return nil, err
}
// Unmarshal the data
var items []*summary.ScanSummaryItem
for _, r := range res {
smBytes := r.Value
var sm summary.ScanSummary
if err := json.Unmarshal(smBytes, &sm); err != nil {
return nil, err
}
items = append(items, &summary.ScanSummaryItem{
CreatedAt: r.CreatedAt,
ScanSummary: &sm,
})
}
return items, nil
}
func InsertScanSummary(db *db.Database, sm *summary.ScanSummary) error {
if sm == nil {
return nil
}
// Marshal the data
bytes, err := json.Marshal(sm)
if err != nil {
return err
}
// Save the data
return db.Gorm().Create(&models.ScanSummary{
Value: bytes,
}).Error
}

View File

@@ -0,0 +1,46 @@
package db_bridge
import (
"github.com/goccy/go-json"
"seanime/internal/database/db"
"seanime/internal/database/models"
hibiketorrent "seanime/internal/extension/hibike/torrent"
)
func GetTorrentstreamHistory(db *db.Database, mId int) (*hibiketorrent.AnimeTorrent, error) {
var history models.TorrentstreamHistory
if err := db.Gorm().Where("media_id = ?", mId).First(&history).Error; err != nil {
return nil, err
}
var torrent hibiketorrent.AnimeTorrent
if err := json.Unmarshal(history.Torrent, &torrent); err != nil {
return nil, err
}
return &torrent, nil
}
func InsertTorrentstreamHistory(db *db.Database, mId int, torrent *hibiketorrent.AnimeTorrent) error {
if torrent == nil {
return nil
}
// Marshal the data
bytes, err := json.Marshal(torrent)
if err != nil {
return err
}
// Get current history
var history models.TorrentstreamHistory
if err := db.Gorm().Where("media_id = ?", mId).First(&history).Error; err == nil {
// Update the history
history.Torrent = bytes
return db.Gorm().Save(&history).Error
}
return db.Gorm().Create(&models.TorrentstreamHistory{
MediaId: mId,
Torrent: bytes,
}).Error
}

View File

@@ -0,0 +1,511 @@
package models
import (
"database/sql/driver"
"errors"
"strconv"
"strings"
"time"
)
type BaseModel struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Token struct {
BaseModel
Value string `json:"value"`
}
type Account struct {
BaseModel
Username string `gorm:"column:username" json:"username"`
Token string `gorm:"column:token" json:"token"`
Viewer []byte `gorm:"column:viewer" json:"viewer"`
}
// +---------------------+
// | LocalFiles |
// +---------------------+
type LocalFiles struct {
BaseModel
Value []byte `gorm:"column:value" json:"value"`
}
// +---------------------+
// | Settings |
// +---------------------+
type Settings struct {
BaseModel
Library *LibrarySettings `gorm:"embedded" json:"library"`
MediaPlayer *MediaPlayerSettings `gorm:"embedded" json:"mediaPlayer"`
Torrent *TorrentSettings `gorm:"embedded" json:"torrent"`
Manga *MangaSettings `gorm:"embedded" json:"manga"`
Anilist *AnilistSettings `gorm:"embedded" json:"anilist"`
ListSync *ListSyncSettings `gorm:"embedded" json:"listSync"`
AutoDownloader *AutoDownloaderSettings `gorm:"embedded" json:"autoDownloader"`
Discord *DiscordSettings `gorm:"embedded" json:"discord"`
Notifications *NotificationSettings `gorm:"embedded" json:"notifications"`
Nakama *NakamaSettings `gorm:"embedded;embeddedPrefix:nakama_" json:"nakama"`
}
type AnilistSettings struct {
//AnilistClientId string `gorm:"column:anilist_client_id" json:"anilistClientId"`
HideAudienceScore bool `gorm:"column:hide_audience_score" json:"hideAudienceScore"`
EnableAdultContent bool `gorm:"column:enable_adult_content" json:"enableAdultContent"`
BlurAdultContent bool `gorm:"column:blur_adult_content" json:"blurAdultContent"`
}
type LibrarySettings struct {
LibraryPath string `gorm:"column:library_path" json:"libraryPath"`
AutoUpdateProgress bool `gorm:"column:auto_update_progress" json:"autoUpdateProgress"`
DisableUpdateCheck bool `gorm:"column:disable_update_check" json:"disableUpdateCheck"`
TorrentProvider string `gorm:"column:torrent_provider" json:"torrentProvider"`
AutoScan bool `gorm:"column:auto_scan" json:"autoScan"`
EnableOnlinestream bool `gorm:"column:enable_onlinestream" json:"enableOnlinestream"`
IncludeOnlineStreamingInLibrary bool `gorm:"column:include_online_streaming_in_library" json:"includeOnlineStreamingInLibrary"`
DisableAnimeCardTrailers bool `gorm:"column:disable_anime_card_trailers" json:"disableAnimeCardTrailers"`
EnableManga bool `gorm:"column:enable_manga" json:"enableManga"`
DOHProvider string `gorm:"column:doh_provider" json:"dohProvider"`
OpenTorrentClientOnStart bool `gorm:"column:open_torrent_client_on_start" json:"openTorrentClientOnStart"`
OpenWebURLOnStart bool `gorm:"column:open_web_url_on_start" json:"openWebURLOnStart"`
RefreshLibraryOnStart bool `gorm:"column:refresh_library_on_start" json:"refreshLibraryOnStart"`
// v2.1+
AutoPlayNextEpisode bool `gorm:"column:auto_play_next_episode" json:"autoPlayNextEpisode"`
// v2.2+
EnableWatchContinuity bool `gorm:"column:enable_watch_continuity" json:"enableWatchContinuity"`
LibraryPaths LibraryPaths `gorm:"column:library_paths;type:text" json:"libraryPaths"`
AutoSyncOfflineLocalData bool `gorm:"column:auto_sync_offline_local_data" json:"autoSyncOfflineLocalData"`
// v2.6+
ScannerMatchingThreshold float64 `gorm:"column:scanner_matching_threshold" json:"scannerMatchingThreshold"`
ScannerMatchingAlgorithm string `gorm:"column:scanner_matching_algorithm" json:"scannerMatchingAlgorithm"`
// v2.9+
AutoSyncToLocalAccount bool `gorm:"column:auto_sync_to_local_account" json:"autoSyncToLocalAccount"`
AutoSaveCurrentMediaOffline bool `gorm:"column:auto_save_current_media_offline" json:"autoSaveCurrentMediaOffline"`
}
func (o *LibrarySettings) GetLibraryPaths() (ret []string) {
ret = make([]string, len(o.LibraryPaths)+1)
ret[0] = o.LibraryPath
if len(o.LibraryPaths) > 0 {
copy(ret[1:], o.LibraryPaths)
}
return
}
type LibraryPaths []string
func (o *LibraryPaths) Scan(src interface{}) error {
str, ok := src.(string)
if !ok {
return errors.New("src value cannot cast to string")
}
*o = strings.Split(str, ",")
return nil
}
func (o LibraryPaths) Value() (driver.Value, error) {
if len(o) == 0 {
return nil, nil
}
return strings.Join(o, ","), nil
}
type NakamaSettings struct {
Enabled bool `gorm:"column:enabled" json:"enabled"`
// Username is the name used to identify a peer or host.
Username string `gorm:"column:username" json:"username"`
// IsHost allows the server to act as a host for other clients. This requires a password to be set.
IsHost bool `gorm:"column:is_host" json:"isHost"`
HostPassword string `gorm:"column:host_password" json:"hostPassword"`
RemoteServerURL string `gorm:"column:remote_server_url" json:"remoteServerURL"`
RemoteServerPassword string `gorm:"column:remote_server_password" json:"remoteServerPassword"`
// IncludeNakamaAnimeLibrary adds the local anime library of the host to the connected clients.
IncludeNakamaAnimeLibrary bool `gorm:"column:include_nakama_anime_library" json:"includeNakamaAnimeLibrary"`
// HostShareLocalAnimeLibrary shares the local anime library to connected clients
HostShareLocalAnimeLibrary bool `gorm:"column:host_share_local_anime_library" json:"hostShareLocalAnimeLibrary"`
// HostUnsharedAnimeIds is a list of anime IDs that should not be shared with connected clients.
HostUnsharedAnimeIds IntSlice `gorm:"column:host_unshared_anime_ids;type:text" json:"hostUnsharedAnimeIds"`
// HostEnablePortForwarding enables port forwarding.
HostEnablePortForwarding bool `gorm:"column:host_enable_port_forwarding" json:"hostEnablePortForwarding"`
}
type IntSlice []int
func (o *IntSlice) Scan(src interface{}) error {
str, ok := src.(string)
if !ok {
return errors.New("src value cannot cast to string")
}
ids := strings.Split(str, ",")
*o = make(IntSlice, len(ids))
for i, id := range ids {
(*o)[i], _ = strconv.Atoi(id)
}
return nil
}
func (o IntSlice) Value() (driver.Value, error) {
if len(o) == 0 {
return nil, nil
}
strs := make([]string, len(o))
for i, id := range o {
strs[i] = strconv.Itoa(id)
}
return strings.Join(strs, ","), nil
}
type MangaSettings struct {
DefaultProvider string `gorm:"column:default_manga_provider" json:"defaultMangaProvider"`
AutoUpdateProgress bool `gorm:"column:manga_auto_update_progress" json:"mangaAutoUpdateProgress"`
LocalSourceDirectory string `gorm:"column:manga_local_source_directory" json:"mangaLocalSourceDirectory"`
}
type MediaPlayerSettings struct {
Default string `gorm:"column:default_player" json:"defaultPlayer"` // "vlc" or "mpc-hc"
Host string `gorm:"column:player_host" json:"host"`
VlcUsername string `gorm:"column:vlc_username" json:"vlcUsername"`
VlcPassword string `gorm:"column:vlc_password" json:"vlcPassword"`
VlcPort int `gorm:"column:vlc_port" json:"vlcPort"`
VlcPath string `gorm:"column:vlc_path" json:"vlcPath"`
MpcPort int `gorm:"column:mpc_port" json:"mpcPort"`
MpcPath string `gorm:"column:mpc_path" json:"mpcPath"`
MpvSocket string `gorm:"column:mpv_socket" json:"mpvSocket"`
MpvPath string `gorm:"column:mpv_path" json:"mpvPath"`
MpvArgs string `gorm:"column:mpv_args" json:"mpvArgs"`
IinaSocket string `gorm:"column:iina_socket" json:"iinaSocket"`
IinaPath string `gorm:"column:iina_path" json:"iinaPath"`
IinaArgs string `gorm:"column:iina_args" json:"iinaArgs"`
}
type TorrentSettings struct {
Default string `gorm:"column:default_torrent_client" json:"defaultTorrentClient"`
QBittorrentPath string `gorm:"column:qbittorrent_path" json:"qbittorrentPath"`
QBittorrentHost string `gorm:"column:qbittorrent_host" json:"qbittorrentHost"`
QBittorrentPort int `gorm:"column:qbittorrent_port" json:"qbittorrentPort"`
QBittorrentUsername string `gorm:"column:qbittorrent_username" json:"qbittorrentUsername"`
QBittorrentPassword string `gorm:"column:qbittorrent_password" json:"qbittorrentPassword"`
QBittorrentTags string `gorm:"column:qbittorrent_tags" json:"qbittorrentTags"`
TransmissionPath string `gorm:"column:transmission_path" json:"transmissionPath"`
TransmissionHost string `gorm:"column:transmission_host" json:"transmissionHost"`
TransmissionPort int `gorm:"column:transmission_port" json:"transmissionPort"`
TransmissionUsername string `gorm:"column:transmission_username" json:"transmissionUsername"`
TransmissionPassword string `gorm:"column:transmission_password" json:"transmissionPassword"`
// v2.1+
ShowActiveTorrentCount bool `gorm:"column:show_active_torrent_count" json:"showActiveTorrentCount"`
// v2.2+
HideTorrentList bool `gorm:"column:hide_torrent_list" json:"hideTorrentList"`
}
type ListSyncSettings struct {
Automatic bool `gorm:"column:automatic_sync" json:"automatic"`
Origin string `gorm:"column:sync_origin" json:"origin"`
}
type DiscordSettings struct {
EnableRichPresence bool `gorm:"column:enable_rich_presence" json:"enableRichPresence"`
EnableAnimeRichPresence bool `gorm:"column:enable_anime_rich_presence" json:"enableAnimeRichPresence"`
EnableMangaRichPresence bool `gorm:"column:enable_manga_rich_presence" json:"enableMangaRichPresence"`
RichPresenceHideSeanimeRepositoryButton bool `gorm:"column:rich_presence_hide_seanime_repository_button" json:"richPresenceHideSeanimeRepositoryButton"`
RichPresenceShowAniListMediaButton bool `gorm:"column:rich_presence_show_anilist_media_button" json:"richPresenceShowAniListMediaButton"`
RichPresenceShowAniListProfileButton bool `gorm:"column:rich_presence_show_anilist_profile_button" json:"richPresenceShowAniListProfileButton"`
RichPresenceUseMediaTitleStatus bool `gorm:"column:rich_presence_use_media_title_status;default:true" json:"richPresenceUseMediaTitleStatus"`
}
type NotificationSettings struct {
DisableNotifications bool `gorm:"column:disable_notifications" json:"disableNotifications"`
DisableAutoDownloaderNotifications bool `gorm:"column:disable_auto_downloader_notifications" json:"disableAutoDownloaderNotifications"`
DisableAutoScannerNotifications bool `gorm:"column:disable_auto_scanner_notifications" json:"disableAutoScannerNotifications"`
}
// +---------------------+
// | MAL |
// +---------------------+
type Mal struct {
BaseModel
Username string `gorm:"column:username" json:"username"`
AccessToken string `gorm:"column:access_token" json:"accessToken"`
RefreshToken string `gorm:"column:refresh_token" json:"refreshToken"`
TokenExpiresAt time.Time `gorm:"column:token_expires_at" json:"tokenExpiresAt"`
}
// +---------------------+
// | Scan Summary |
// +---------------------+
type ScanSummary struct {
BaseModel
Value []byte `gorm:"column:value" json:"value"`
}
// +---------------------+
// | Auto downloader |
// +---------------------+
type AutoDownloaderRule struct {
BaseModel
Value []byte `gorm:"column:value" json:"value"`
}
type AutoDownloaderItem struct {
BaseModel
RuleID uint `gorm:"column:rule_id" json:"ruleId"`
MediaID int `gorm:"column:media_id" json:"mediaId"`
Episode int `gorm:"column:episode" json:"episode"`
Link string `gorm:"column:link" json:"link"`
Hash string `gorm:"column:hash" json:"hash"`
Magnet string `gorm:"column:magnet" json:"magnet"`
TorrentName string `gorm:"column:torrent_name" json:"torrentName"`
Downloaded bool `gorm:"column:downloaded" json:"downloaded"`
}
type AutoDownloaderSettings struct {
Provider string `gorm:"column:auto_downloader_provider" json:"provider"`
Interval int `gorm:"column:auto_downloader_interval" json:"interval"`
Enabled bool `gorm:"column:auto_downloader_enabled" json:"enabled"`
DownloadAutomatically bool `gorm:"column:auto_downloader_download_automatically" json:"downloadAutomatically"`
EnableEnhancedQueries bool `gorm:"column:auto_downloader_enable_enhanced_queries" json:"enableEnhancedQueries"`
EnableSeasonCheck bool `gorm:"column:auto_downloader_enable_season_check" json:"enableSeasonCheck"`
UseDebrid bool `gorm:"column:auto_downloader_use_debrid" json:"useDebrid"`
}
// +---------------------+
// | Media Entry |
// +---------------------+
type SilencedMediaEntry struct {
BaseModel
}
// +---------------------+
// | Theme |
// +---------------------+
type Theme struct {
BaseModel
// Main
EnableColorSettings bool `gorm:"column:enable_color_settings" json:"enableColorSettings"`
BackgroundColor string `gorm:"column:background_color" json:"backgroundColor"`
AccentColor string `gorm:"column:accent_color" json:"accentColor"`
SidebarBackgroundColor string `gorm:"column:sidebar_background_color" json:"sidebarBackgroundColor"` // DEPRECATED
AnimeEntryScreenLayout string `gorm:"column:anime_entry_screen_layout" json:"animeEntryScreenLayout"` // DEPRECATED
ExpandSidebarOnHover bool `gorm:"column:expand_sidebar_on_hover" json:"expandSidebarOnHover"`
HideTopNavbar bool `gorm:"column:hide_top_navbar" json:"hideTopNavbar"`
EnableMediaCardBlurredBackground bool `gorm:"column:enable_media_card_blurred_background" json:"enableMediaCardBlurredBackground"`
// Note: These are named "libraryScreen" but are used on all pages
LibraryScreenCustomBackgroundImage string `gorm:"column:library_screen_custom_background_image" json:"libraryScreenCustomBackgroundImage"`
LibraryScreenCustomBackgroundOpacity int `gorm:"column:library_screen_custom_background_opacity" json:"libraryScreenCustomBackgroundOpacity"`
// Anime
SmallerEpisodeCarouselSize bool `gorm:"column:smaller_episode_carousel_size" json:"smallerEpisodeCarouselSize"`
// Library Screen (Anime & Manga)
// LibraryScreenBannerType: "dynamic", "custom"
LibraryScreenBannerType string `gorm:"column:library_screen_banner_type" json:"libraryScreenBannerType"`
LibraryScreenCustomBannerImage string `gorm:"column:library_screen_custom_banner_image" json:"libraryScreenCustomBannerImage"`
LibraryScreenCustomBannerPosition string `gorm:"column:library_screen_custom_banner_position" json:"libraryScreenCustomBannerPosition"`
LibraryScreenCustomBannerOpacity int `gorm:"column:library_screen_custom_banner_opacity" json:"libraryScreenCustomBannerOpacity"`
DisableLibraryScreenGenreSelector bool `gorm:"column:disable_library_screen_genre_selector" json:"disableLibraryScreenGenreSelector"`
LibraryScreenCustomBackgroundBlur string `gorm:"column:library_screen_custom_background_blur" json:"libraryScreenCustomBackgroundBlur"`
EnableMediaPageBlurredBackground bool `gorm:"column:enable_media_page_blurred_background" json:"enableMediaPageBlurredBackground"`
DisableSidebarTransparency bool `gorm:"column:disable_sidebar_transparency" json:"disableSidebarTransparency"`
UseLegacyEpisodeCard bool `gorm:"column:use_legacy_episode_card" json:"useLegacyEpisodeCard"` // DEPRECATED
DisableCarouselAutoScroll bool `gorm:"column:disable_carousel_auto_scroll" json:"disableCarouselAutoScroll"`
// v2.6+
MediaPageBannerType string `gorm:"column:media_page_banner_type" json:"mediaPageBannerType"`
MediaPageBannerSize string `gorm:"column:media_page_banner_size" json:"mediaPageBannerSize"`
MediaPageBannerInfoBoxSize string `gorm:"column:media_page_banner_info_box_size" json:"mediaPageBannerInfoBoxSize"`
// v2.7+
ShowEpisodeCardAnimeInfo bool `gorm:"column:show_episode_card_anime_info" json:"showEpisodeCardAnimeInfo"`
ContinueWatchingDefaultSorting string `gorm:"column:continue_watching_default_sorting" json:"continueWatchingDefaultSorting"`
AnimeLibraryCollectionDefaultSorting string `gorm:"column:anime_library_collection_default_sorting" json:"animeLibraryCollectionDefaultSorting"`
MangaLibraryCollectionDefaultSorting string `gorm:"column:manga_library_collection_default_sorting" json:"mangaLibraryCollectionDefaultSorting"`
ShowAnimeUnwatchedCount bool `gorm:"column:show_anime_unwatched_count" json:"showAnimeUnwatchedCount"`
ShowMangaUnreadCount bool `gorm:"column:show_manga_unread_count" json:"showMangaUnreadCount"`
// v2.8+
HideEpisodeCardDescription bool `gorm:"column:hide_episode_card_description" json:"hideEpisodeCardDescription"`
HideDownloadedEpisodeCardFilename bool `gorm:"column:hide_downloaded_episode_card_filename" json:"hideDownloadedEpisodeCardFilename"`
CustomCSS string `gorm:"column:custom_css" json:"customCSS"`
MobileCustomCSS string `gorm:"column:mobile_custom_css" json:"mobileCustomCSS"`
// v2.9+
UnpinnedMenuItems StringSlice `gorm:"column:unpinned_menu_items;type:text" json:"unpinnedMenuItems"`
}
// +---------------------+
// | Playlist |
// +---------------------+
type PlaylistEntry struct {
BaseModel
Name string `gorm:"column:name" json:"name"`
Value []byte `gorm:"column:value" json:"value"`
}
// +------------------------+
// | Chapter Download Queue |
// +------------------------+
type ChapterDownloadQueueItem struct {
BaseModel
Provider string `gorm:"column:provider" json:"provider"`
MediaID int `gorm:"column:media_id" json:"mediaId"`
ChapterID string `gorm:"column:chapter_id" json:"chapterId"`
ChapterNumber string `gorm:"column:chapter_number" json:"chapterNumber"`
PageData []byte `gorm:"column:page_data" json:"pageData"` // Contains map of page index to page details
Status string `gorm:"column:status" json:"status"`
}
// +---------------------+
// | MediaStream |
// +---------------------+
type MediastreamSettings struct {
BaseModel
// DEVNOTE: Should really be "Enabled"
TranscodeEnabled bool `gorm:"column:transcode_enabled" json:"transcodeEnabled"`
TranscodeHwAccel string `gorm:"column:transcode_hw_accel" json:"transcodeHwAccel"`
TranscodeThreads int `gorm:"column:transcode_threads" json:"transcodeThreads"`
TranscodePreset string `gorm:"column:transcode_preset" json:"transcodePreset"`
DisableAutoSwitchToDirectPlay bool `gorm:"column:disable_auto_switch_to_direct_play" json:"disableAutoSwitchToDirectPlay"`
DirectPlayOnly bool `gorm:"column:direct_play_only" json:"directPlayOnly"`
PreTranscodeEnabled bool `gorm:"column:pre_transcode_enabled" json:"preTranscodeEnabled"`
PreTranscodeLibraryDir string `gorm:"column:pre_transcode_library_dir" json:"preTranscodeLibraryDir"`
FfmpegPath string `gorm:"column:ffmpeg_path" json:"ffmpegPath"`
FfprobePath string `gorm:"column:ffprobe_path" json:"ffprobePath"`
// v2.2+
TranscodeHwAccelCustomSettings string `gorm:"column:transcode_hw_accel_custom_settings" json:"transcodeHwAccelCustomSettings"`
//TranscodeTempDir string `gorm:"column:transcode_temp_dir" json:"transcodeTempDir"` // DEPRECATED
}
// +---------------------+
// | TorrentStream |
// +---------------------+
type TorrentstreamSettings struct {
BaseModel
Enabled bool `gorm:"column:enabled" json:"enabled"`
AutoSelect bool `gorm:"column:auto_select" json:"autoSelect"`
PreferredResolution string `gorm:"column:preferred_resolution" json:"preferredResolution"`
DisableIPV6 bool `gorm:"column:disable_ipv6" json:"disableIPV6"`
DownloadDir string `gorm:"column:download_dir" json:"downloadDir"`
AddToLibrary bool `gorm:"column:add_to_library" json:"addToLibrary"`
TorrentClientHost string `gorm:"column:torrent_client_host" json:"torrentClientHost"`
TorrentClientPort int `gorm:"column:torrent_client_port" json:"torrentClientPort"`
StreamingServerHost string `gorm:"column:streaming_server_host" json:"streamingServerHost"`
StreamingServerPort int `gorm:"column:streaming_server_port" json:"streamingServerPort"`
//FallbackToTorrentStreamingView bool `gorm:"column:fallback_to_torrent_streaming_view" json:"fallbackToTorrentStreamingView"` // DEPRECATED
IncludeInLibrary bool `gorm:"column:include_in_library" json:"includeInLibrary"`
// v2.6+
StreamUrlAddress string `gorm:"column:stream_url_address" json:"streamUrlAddress"`
// v2.7+
SlowSeeding bool `gorm:"column:slow_seeding" json:"slowSeeding"`
}
type TorrentstreamHistory struct {
BaseModel
MediaId int `gorm:"column:media_id" json:"mediaId"`
Torrent []byte `gorm:"column:torrent" json:"torrent"`
}
// +---------------------+
// | Filler |
// +---------------------+
type MediaFiller struct {
BaseModel
Provider string `gorm:"column:provider" json:"provider"`
Slug string `gorm:"column:slug" json:"slug"`
MediaID int `gorm:"column:media_id" json:"mediaId"`
LastFetchedAt time.Time `gorm:"column:last_fetched_at" json:"lastFetchedAt"`
Data []byte `gorm:"column:data" json:"data"`
}
// +---------------------+
// | Manga |
// +---------------------+
type MangaMapping struct {
BaseModel
Provider string `gorm:"column:provider" json:"provider"`
MediaID int `gorm:"column:media_id" json:"mediaId"`
MangaID string `gorm:"column:manga_id" json:"mangaId"` // ID from search result, used to fetch chapters
}
type MangaChapterContainer struct {
BaseModel
Provider string `gorm:"column:provider" json:"provider"`
MediaID int `gorm:"column:media_id" json:"mediaId"`
ChapterID string `gorm:"column:chapter_id" json:"chapterId"`
Data []byte `gorm:"column:data" json:"data"`
}
// +---------------------+
// | Online streaming |
// +---------------------+
type OnlinestreamMapping struct {
BaseModel
Provider string `gorm:"column:provider" json:"provider"`
MediaID int `gorm:"column:media_id" json:"mediaId"`
AnimeID string `gorm:"column:anime_id" json:"anime_id"` // ID from search result, used to fetch episodes
}
// +---------------------+
// | Debrid |
// +---------------------+
type DebridSettings struct {
BaseModel
Enabled bool `gorm:"column:enabled" json:"enabled"`
Provider string `gorm:"column:provider" json:"provider"`
ApiKey string `gorm:"column:api_key" json:"apiKey"`
//FallbackToDebridStreamingView bool `gorm:"column:fallback_to_debrid_streaming_view" json:"fallbackToDebridStreamingView"` // DEPRECATED
IncludeDebridStreamInLibrary bool `gorm:"column:include_debrid_stream_in_library" json:"includeDebridStreamInLibrary"`
StreamAutoSelect bool `gorm:"column:stream_auto_select" json:"streamAutoSelect"`
StreamPreferredResolution string `gorm:"column:stream_preferred_resolution" json:"streamPreferredResolution"`
}
type DebridTorrentItem struct {
BaseModel
TorrentItemID string `gorm:"column:torrent_item_id" json:"torrentItemId"`
Destination string `gorm:"column:destination" json:"destination"`
Provider string `gorm:"column:provider" json:"provider"`
MediaId int `gorm:"column:media_id" json:"mediaId"`
}
// +---------------------+
// | Plugin |
// +---------------------+
type PluginData struct {
BaseModel
PluginID string `gorm:"column:plugin_id;index" json:"pluginId"`
Data []byte `gorm:"column:data" json:"data"`
}
///////////////////////////////////////////////////////////////////////////
type StringSlice []string
func (o *StringSlice) Scan(src interface{}) error {
str, ok := src.(string)
if !ok {
return errors.New("src value cannot cast to string")
}
*o = strings.Split(str, ",")
return nil
}
func (o StringSlice) Value() (driver.Value, error) {
if len(o) == 0 {
return nil, nil
}
return strings.Join(o, ","), nil
}

View File

@@ -0,0 +1,93 @@
package models
func (s *Settings) GetMediaPlayer() *MediaPlayerSettings {
if s == nil || s.MediaPlayer == nil {
return &MediaPlayerSettings{}
}
return s.MediaPlayer
}
func (s *Settings) GetTorrent() *TorrentSettings {
if s == nil || s.Torrent == nil {
return &TorrentSettings{}
}
return s.Torrent
}
func (s *Settings) GetAnilist() *AnilistSettings {
if s == nil || s.Anilist == nil {
return &AnilistSettings{}
}
return s.Anilist
}
func (s *Settings) GetManga() *MangaSettings {
if s == nil || s.Manga == nil {
return &MangaSettings{}
}
return s.Manga
}
func (s *Settings) GetLibrary() *LibrarySettings {
if s == nil || s.Library == nil {
return &LibrarySettings{}
}
return s.Library
}
func (s *Settings) GetListSync() *ListSyncSettings {
if s == nil || s.ListSync == nil {
return &ListSyncSettings{}
}
return s.ListSync
}
func (s *Settings) GetAutoDownloader() *AutoDownloaderSettings {
if s == nil || s.AutoDownloader == nil {
return &AutoDownloaderSettings{}
}
return s.AutoDownloader
}
func (s *Settings) GetDiscord() *DiscordSettings {
if s == nil || s.Discord == nil {
return &DiscordSettings{}
}
return s.Discord
}
func (s *Settings) GetNotifications() *NotificationSettings {
if s == nil || s.Notifications == nil {
return &NotificationSettings{}
}
return s.Notifications
}
func (s *Settings) GetNakama() *NakamaSettings {
if s == nil || s.Nakama == nil {
return &NakamaSettings{}
}
return s.Nakama
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (s *Settings) GetSensitiveValues() []string {
if s == nil {
return []string{}
}
return []string{
s.GetMediaPlayer().VlcPassword,
s.GetTorrent().QBittorrentPassword,
s.GetTorrent().TransmissionPassword,
}
}
func (s *DebridSettings) GetSensitiveValues() []string {
if s == nil {
return []string{}
}
return []string{
s.ApiKey,
}
}