Source

ukiyo / config.go

Full commit
// abstracts access to the config database, which is also used to store series
// and chapter information so that not all queries are live against all sites

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"os"
	"path/filepath"
	"runtime"
)

type Xdg struct {
	DATA_HOME   string
	CONFIG_HOME string
	Initialized bool
}

type Site struct {
	Name     string
	Url      string
	Priority int
	Updated  int64
}

type Config struct {
	DownloadPath string
	SiteOrder    []string
	path         string
	db           *sql.DB
}

type Series struct {
	Name    string
	Url     string
	Site    string
	Key     string
	Updated int64
}

type Chapter struct {
	Series  string
	Site    string
	Name    string
	Url     string
	Number  string
	Numberf float64
}

var xdg Xdg
var config Config

var DEFAULT_DOWNLOAD_PATH = expandpath("~/Downloads")

func getenv(key, default_ string) string {
	value := os.Getenv(key)
	if len(value) == 0 {
		value = default_
	}
	return value
}

// initialize platform dependent data/config paths
func (x *Xdg) init() {
	if x.Initialized {
		return
	}
	switch runtime.GOOS {
	case "linux":
		x.DATA_HOME = getenv("XDG_DATA_HOME", expandpath("~/.local/share"))
		x.CONFIG_HOME = getenv("XDG_CONFIG_HOME", expandpath("~/.config"))
	case "darwin", "osx":
		x.DATA_HOME = expandpath("~/Library/Application Support/")
		x.CONFIG_HOME = expandpath("~/Library/Preferences/")
	}

	x.Initialized = true
}

func (c *Config) Open() *sql.DB {
	db, err := sql.Open("sqlite3", c.path)
	if err != nil {
		panic(fmt.Sprintf("%q", err))
	}
	c.db = db
	return db
}

func (c *Config) GetVal(key string) (string, error) {
	var value string
	row := c.db.QueryRow("select value from config where key = ?", key)
	err := row.Scan(&value)
	return value, err
}

func (c *Config) SetVal(key, val string) {
	Exec(c.db, "update config set value=? where key=?", val, key)
}

func (c *Config) init() {
	var err error
	if !xdg.Initialized {
		xdg.init()
	}
	configPath := filepath.Join(xdg.CONFIG_HOME, "ukiyo/")
	os.MkdirAll(configPath, 0755)

	c.path = filepath.Join(configPath, "config.db")
	c.Open()

	if !exists(c.path) {
		c.initDb()
	}

	c.DownloadPath, err = c.GetVal("DownloadPath")
	if err != nil {
		fmt.Errorf("Could not read key 'DownloadPath' from config: %q\n", err)
	}

	rows := Query(c.db, "select name from sites order by priority")
	for rows.Next() {
		var s string
		rows.Scan(&s)
		c.SiteOrder = append(c.SiteOrder, s)
	}

}

// Abstract interface covering Exec & Query so that transactions and the db can
// be used by the same functions... 
type Executor interface {
	Exec(query string, args ...interface{}) (sql.Result, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
}

// helper function which creates a new transaction or panics on error
func Begin(db *sql.DB) *sql.Tx {
	tx, err := db.Begin()
	if err != nil {
		panic(err)
	}
	return tx
}

// helper function which runs Exec on an Executor and panics on error
func Exec(e Executor, query string, args ...interface{}) sql.Result {
	ret, err := e.Exec(query, args...)
	if err != nil {
		panic(err)
	}
	return ret
}

// helper function which runs Query on an Executor and panics on error
func Query(e Executor, query string, args ...interface{}) *sql.Rows {
	ret, err := e.Query(query, args...)
	if err != nil {
		panic(err)
	}
	return ret
}

func (c *Config) initDb() {
	tables := []string{
		"create table config (key text primary key, value text)",
		"create table watchlist (name text primary key, chapter text)",
		"create table sites (name text primary key, url text, priority integer, updated integer default 0)",
		"create table series (name text, key text, url text primary key, site text, updated integer default 0)",
		"create table chapters (name text, number text, url text primary key, series text, site text)",
	}
	// start a transaction;  sqlite is slow as hell without them
	tx := Begin(c.db)
	defer tx.Commit()

	// create tables
	for _, t := range tables {
		Exec(tx, t)
	}

	Exec(tx, "insert into config (key, value) values (?, ?)", "DownloadPath", DEFAULT_DOWNLOAD_PATH)

	addSite := "insert into sites (name, url, priority) values (?, ?, ?)"
	tx.Exec(addSite, "manga-access", "http://www.manga-access.com", 1)
	tx.Exec(addSite, "mangahere", "http://www.mangahere.com", 2)
	tx.Exec(addSite, "mangareader", "http://www.mangareader.net", 3)
	tx.Exec(addSite, "mangafox", "http://www.mangafox.me", 4)

}

func (c *Config) SetDownloadPath(path string) {
	c.SetVal("DownloadPath", path)
}

func (c *Config) AddSite(name, url string, priority int) error {
	_, err := c.db.Exec("insert into sites (name, url, priority) values (?, ?, ?)",
		name, url, priority)
	return err
}

func (c *Config) RemoveSite(name string) error {
	if len(name) == 0 {
		return fmt.Errorf("Error: name of site to delete must be provided.")
	}
	_, err := c.db.Exec("DELETE FROM sites WHERE name=?", name)
	return err
}

func (c *Config) SetSitePriority(name string, priority int) {
	Exec(c.db, "update sites set priority=? where name=?", priority, name)
}

// Convenient interface for fetching a list of series objects from the db
func QuerySeries(db *sql.DB, query string, args ...interface{}) []*Series {
	series := make([]*Series, 0)
	rows, err := db.Query(query, args...)
	if err != nil {
		fmt.Printf("Error occured fetching series: %s\n", err)
		return series
	}
	for rows.Next() {
		s := new(Series)
		err = rows.Scan(&s.Name, &s.Key, &s.Url, &s.Site, &s.Updated)
		if err != nil {
			fmt.Printf("Error occured scanning series\n")
		} else {
			series = append(series, s)
		}
	}
	return series
}

func init() {
	config.init()
}