Add Studio to movie and fix movie schema (#458)

* Add movie migration
* Update server and UI code for type changes
* Add studio to movies
* Movie blobs to end
* Document movie duration
* Add filtering on movie studio
This commit is contained in:
WithoutPants
2020-04-22 11:22:14 +10:00
committed by GitHub
parent f21e04dcbc
commit eee7adfb85
30 changed files with 531 additions and 144 deletions

View File

@@ -5,11 +5,11 @@ import (
"errors"
"fmt"
"os"
"regexp"
"time"
"github.com/gobuffalo/packr/v2"
"github.com/golang-migrate/migrate/v4"
sqlite3mig "github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source"
"github.com/jmoiron/sqlx"
sqlite3 "github.com/mattn/go-sqlite3"
@@ -19,14 +19,14 @@ import (
var DB *sqlx.DB
var dbPath string
var appSchemaVersion uint = 7
var appSchemaVersion uint = 8
var databaseSchemaVersion uint
const sqlite3Driver = "sqlite3_regexp"
const sqlite3Driver = "sqlite3ex"
func init() {
// register custom driver with regexp function
registerRegexpFunc()
registerCustomDriver()
}
func Initialize(databasePath string) {
@@ -55,14 +55,25 @@ func Initialize(databasePath string) {
}
}
const disableForeignKeys = false
DB = open(databasePath, disableForeignKeys)
}
func open(databasePath string, disableForeignKeys bool) *sqlx.DB {
// https://github.com/mattn/go-sqlite3
conn, err := sqlx.Open(sqlite3Driver, "file:"+databasePath+"?_fk=true")
url := "file:" + databasePath
if !disableForeignKeys {
url += "?_fk=true"
}
conn, err := sqlx.Open(sqlite3Driver, url)
conn.SetMaxOpenConns(25)
conn.SetMaxIdleConns(4)
if err != nil {
logger.Fatalf("db.Open(): %q\n", err)
}
DB = conn
return conn
}
func Reset(databasePath string) error {
@@ -97,6 +108,10 @@ func Backup(backupPath string) error {
return nil
}
func RestoreFromBackup(backupPath string) error {
return os.Rename(backupPath, dbPath)
}
// Migrate the database
func NeedsMigration() bool {
return databaseSchemaVersion != appSchemaVersion
@@ -123,10 +138,21 @@ func getMigrate() (*migrate.Migrate, error) {
databasePath := utils.FixWindowsPath(dbPath)
s, _ := WithInstance(packrSource)
return migrate.NewWithSourceInstance(
const disableForeignKeys = true
conn := open(databasePath, disableForeignKeys)
driver, err := sqlite3mig.WithInstance(conn.DB, &sqlite3mig.Config{})
if err != nil {
return nil, err
}
// use sqlite3Driver so that migration has access to durationToTinyInt
return migrate.NewWithInstance(
"packr2",
s,
fmt.Sprintf("sqlite3://%s", "file:"+databasePath),
databasePath,
driver,
)
}
@@ -153,6 +179,8 @@ func RunMigrations() error {
if stepNumber != 0 {
err = m.Steps(int(stepNumber))
if err != nil {
// migration failed
m.Close()
return err
}
}
@@ -164,15 +192,23 @@ func RunMigrations() error {
return nil
}
func registerRegexpFunc() {
regexFn := func(re, s string) (bool, error) {
return regexp.MatchString(re, s)
}
func registerCustomDriver() {
sql.Register(sqlite3Driver,
&sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
return conn.RegisterFunc("regexp", regexFn, true)
funcs := map[string]interface{}{
"regexp": regexFn,
"durationToTinyInt": durationToTinyIntFn,
}
for name, fn := range funcs {
if err := conn.RegisterFunc(name, fn, true); err != nil {
return fmt.Errorf("Error registering function %s: %s", name, err.Error())
}
}
return nil
},
})
},
)
}

37
pkg/database/functions.go Normal file
View File

@@ -0,0 +1,37 @@
package database
import (
"regexp"
"strconv"
"strings"
)
func regexFn(re, s string) (bool, error) {
return regexp.MatchString(re, s)
}
func durationToTinyIntFn(str string) (int64, error) {
splits := strings.Split(str, ":")
if len(splits) > 3 {
return 0, nil
}
seconds := 0
factor := 1
for len(splits) > 0 {
// pop the last split
var thisSplit string
thisSplit, splits = splits[len(splits)-1], splits[:len(splits)-1]
thisInt, err := strconv.Atoi(thisSplit)
if err != nil {
return 0, nil
}
seconds += factor * thisInt
factor *= 60
}
return int64(seconds), nil
}

View File

@@ -0,0 +1,106 @@
ALTER TABLE `movies` rename to `_movies_old`;
ALTER TABLE `movies_scenes` rename to `_movies_scenes_old`;
DROP INDEX IF EXISTS `movies_checksum_unique`;
DROP INDEX IF EXISTS `index_movie_id_scene_index_unique`;
DROP INDEX IF EXISTS `index_movies_scenes_on_movie_id`;
DROP INDEX IF EXISTS `index_movies_scenes_on_scene_id`;
-- recreate the movies table with fixed column types and constraints
CREATE TABLE `movies` (
`id` integer not null primary key autoincrement,
-- add not null
`name` varchar(255) not null,
`aliases` varchar(255),
-- varchar(6) -> integer
`duration` integer,
`date` date,
-- varchar(1) -> tinyint
`rating` tinyint,
`studio_id` integer,
`director` varchar(255),
`synopsis` text,
`checksum` varchar(255) not null,
`url` varchar(255),
`created_at` datetime not null,
`updated_at` datetime not null,
`front_image` blob not null,
`back_image` blob,
foreign key(`studio_id`) references `studios`(`id`) on delete set null
);
CREATE TABLE `movies_scenes` (
`movie_id` integer,
`scene_id` integer,
-- varchar(2) -> tinyint
`scene_index` tinyint,
foreign key(`movie_id`) references `movies`(`id`) on delete cascade,
foreign key(`scene_id`) references `scenes`(`id`) on delete cascade
);
-- add unique index on movie name
CREATE UNIQUE INDEX `movies_name_unique` on `movies` (`name`);
CREATE UNIQUE INDEX `movies_checksum_unique` on `movies` (`checksum`);
-- remove unique index on movies_scenes
CREATE INDEX `index_movies_scenes_on_movie_id` on `movies_scenes` (`movie_id`);
CREATE INDEX `index_movies_scenes_on_scene_id` on `movies_scenes` (`scene_id`);
CREATE INDEX `index_movies_on_studio_id` on `movies` (`studio_id`);
-- custom functions cannot accept NULL values, so massage the old data
UPDATE `_movies_old` set `duration` = 0 WHERE `duration` IS NULL;
-- now populate from the old tables
INSERT INTO `movies`
(
`id`,
`name`,
`aliases`,
`duration`,
`date`,
`rating`,
`director`,
`synopsis`,
`front_image`,
`back_image`,
`checksum`,
`url`,
`created_at`,
`updated_at`
)
SELECT
`id`,
`name`,
`aliases`,
durationToTinyInt(`duration`),
`date`,
CAST(`rating` as tinyint),
`director`,
`synopsis`,
`front_image`,
`back_image`,
`checksum`,
`url`,
`created_at`,
`updated_at`
FROM `_movies_old`
-- ignore null named movies
WHERE `name` is not null;
-- durationToTinyInt returns 0 if it cannot parse the string
-- set these values to null instead
UPDATE `movies` SET `duration` = NULL WHERE `duration` = 0;
INSERT INTO `movies_scenes`
(
`movie_id`,
`scene_id`,
`scene_index`
)
SELECT
`movie_id`,
`scene_id`,
CAST(`scene_index` as tinyint)
FROM `_movies_scenes_old`;
-- drop old tables
DROP TABLE `_movies_scenes_old`;
DROP TABLE `_movies_old`;