<commit_before>// Package implements creation of XLSX simple spreadsheet files

package xlsx

import (
	"archive/zip"
	"bufio"
	"bytes"
	"fmt"
	"html"
	"io"
	"os"
	"strconv"
	"time"
)

type CellType uint

// Basic spreadsheet cell types
const (
	CellTypeNumber CellType = iota
	CellTypeString
	CellTypeDatetime
	CellTypeInlineString
)

// XLSX Spreadsheet Cell
type Cell struct {
	Type  CellType
	Value string
}

// XLSX Spreadsheet Row
type Row struct {
	Cells []Cell
}

// XLSX Spreadsheet Column
type Column struct {
	Name  string
	Width uint64
}

// XLSX Spreadsheet Document Properties
type DocumentInfo struct {
	CreatedBy  string
	ModifiedBy string
	CreatedAt  time.Time
	ModifiedAt time.Time
}

// XLSX Spreadsheet
type Sheet struct {
	Title           string
	columns         []Column
	rows            []Row
	sharedStringMap map[string]int
	sharedStrings   []string
	DocumentInfo    DocumentInfo
}

// Create a sheet with no dimensions
func NewSheet() Sheet {
	c := make([]Column, 0)
	r := make([]Row, 0)
	ssm := make(map[string]int)
	sst := make([]string, 0)

	s := Sheet{
		Title:           "Data",
		columns:         c,
		rows:            r,
		sharedStringMap: ssm,
		sharedStrings:   sst,
	}

	return s
}

// Create a sheet with dimensions derived from the given columns
func NewSheetWithColumns(c []Column) Sheet {
	r := make([]Row, 0)
	ssm := make(map[string]int)
	sst := make([]string, 0)

	s := Sheet{
		Title:           "Data",
		columns:         c,
		rows:            r,
		sharedStringMap: ssm,
		sharedStrings:   sst,
	}

	s.DocumentInfo.CreatedBy = "xlsx.go"
	s.DocumentInfo.CreatedAt = time.Now()

	s.DocumentInfo.ModifiedBy = s.DocumentInfo.CreatedBy
	s.DocumentInfo.ModifiedAt = s.DocumentInfo.CreatedAt

	return s
}

// Create a new row with a length caculated by the sheets known column count
func (s *Sheet) NewRow() Row {
	c := make([]Cell, len(s.columns))
	r := Row{
		Cells: c,
	}
	return r
}

// Append a row to the sheet
func (s *Sheet) AppendRow(r Row) error {
	if len(r.Cells) != len(s.columns) {
		return fmt.Errorf("the given row has %d cells and %d were expected", len(r.Cells), len(s.columns))
	}

	cells := make([]Cell, len(s.columns))

	for n, c := range r.Cells {
		cells[n].Type = c.Type
		cells[n].Value = c.Value

		if cells[n].Type == CellTypeString {
			// calculate string reference
			cells[n].Value = html.EscapeString(cells[n].Value)
			i, exists := s.sharedStringMap[cells[n].Value]
			if !exists {
				i = len(s.sharedStrings)
				s.sharedStringMap[cells[n].Value] = i
				s.sharedStrings = append(s.sharedStrings, cells[n].Value)
			}
			cells[n].Value = strconv.Itoa(i)
		}
	}

	row := s.NewRow()
	row.Cells = cells

	s.rows = append(s.rows, row)

	return nil
}

// Get the Shared Strings in the order they were added to the map
func (s *Sheet) SharedStrings() []string {
	return s.sharedStrings
}

// Given zero-based array indices output the Excel cell reference. For
// example (0,0) => "A1"; (2,2) => "C3"; (26,45) => "AA46"
func CellIndex(x, y uint64) (string, uint64) {
	return colName(x), (y + 1)
}

// From a zero-based column number return the Excel column name.
// For example: 0 => "A"; 2 => "C"; 26 => "AA"
func colName(n uint64) string {
	var s string
	n += 1

	for n > 0 {
		n -= 1
		s = string(65+(n%26)) + s
		n /= 26
	}

	return s
}

// Convert time to the OLE Automation format.
func OADate(d time.Time) string {
	epoch := time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
	nsPerDay := 24 * time.Hour

	v := -1 * float64(epoch.Sub(d)) / float64(nsPerDay)

	// TODO: deal with dates before epoch
	// e.g. http://stackoverflow.com/questions/15549823/oadate-to-milliseconds-timestamp-in-javascript/15550284#15550284

	if d.Hour() == 0 && d.Minute() == 0 && d.Second() == 0 {
		return fmt.Sprintf("%d", int64(v))
	} else {
		return fmt.Sprintf("%f", v)
	}
}

// Create filename and save the XLSX file
func (s *Sheet) SaveToFile(filename string) error {
	outputfile, err := os.Create(filename)
	if err != nil {
		return err
	}
	w := bufio.NewWriter(outputfile)
	err = s.SaveToWriter(w)
	defer w.Flush()
	return err
}

// Save the XLSX file to the given writer
func (s *Sheet) SaveToWriter(w io.Writer) error {

	ww := NewWorkbookWriter(w)

	sw, err := ww.NewSheetWriter(s)
	if err != nil {
		return err
	}

	err = sw.WriteRows(s.rows)
	if err != nil {
		return err
	}

	err = ww.Close()

	return err
}

// Handles the writing of an XLSX workbook
type WorkbookWriter struct {
	zipWriter     *zip.Writer
	sheetWriter   *SheetWriter
	headerWritten bool
	closed        bool
}

// NewWorkbookWriter creates a new WorkbookWriter, which SheetWriters will
// operate on. It must be closed when all Sheets have been written.
func NewWorkbookWriter(w io.Writer) *WorkbookWriter {
	return &WorkbookWriter{zip.NewWriter(w), nil, false, false}
}

// Write the header files of the workbook
func (ww *WorkbookWriter) WriteHeader(s *Sheet) error {
	if ww.closed {
		panic("Can not write to closed WorkbookWriter")
	}

	if ww.headerWritten {
		panic("Workbook header already written")
	}

	z := ww.zipWriter

	f, err := z.Create("[Content_Types].xml")
	err = TemplateContentTypes.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("docProps/app.xml")
	err = TemplateApp.Execute(f, s)
	if err != nil {
		return err
	}

	f, err = z.Create("docProps/core.xml")
	err = TemplateCore.Execute(f, s.DocumentInfo)
	if err != nil {
		return err
	}

	f, err = z.Create("_rels/.rels")
	err = TemplateRelationships.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/workbook.xml")
	err = TemplateWorkbook.Execute(f, s)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/_rels/workbook.xml.rels")
	err = TemplateWorkbookRelationships.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/styles.xml")
	err = TemplateStyles.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/sharedStrings.xml")
	err = TemplateStringLookups.Execute(f, s.SharedStrings())
	if err != nil {
		return err
	}

	return nil
}

// Closes the WorkbookWriter
func (ww *WorkbookWriter) Close() error {
	if ww.closed {
		panic("WorkbookWriter already closed")
	}

	if ww.sheetWriter != nil {
		err := ww.sheetWriter.Close()
		if err != nil {
			return err
		}
	}

	ww.closed = true

	return ww.zipWriter.Close()
}

// NewSheetWriter creates a new SheetWriter in this workbook using the given sheet.
// It returns a SheetWriter to which rows can be written.
// All rows must be written to the SheetWriter before the next call to NewSheetWriter,
// as this will automatically close the previous SheetWriter.
func (ww *WorkbookWriter) NewSheetWriter(s *Sheet) (*SheetWriter, error) {
	if ww.closed {
		panic("Can not write to closed WorkbookWriter")
	}

	if !ww.headerWritten {
		err := ww.WriteHeader(s)
		if err != nil {
			return nil, err
		}
	}

	f, err := ww.zipWriter.Create("xl/worksheets/" + "sheet1" + ".xml")
	sw := &SheetWriter{f, err, 0, 0, false}

	if ww.sheetWriter != nil {
		err = ww.sheetWriter.Close()
		if err != nil {
			return nil, err
		}
	}

	ww.sheetWriter = sw
	err = sw.WriteHeader(s)

	return sw, err
}

// Handles the writing of a sheet
type SheetWriter struct {
	f            io.Writer
	err          error
	currentIndex uint64
	maxNCols     uint64
	closed       bool
}

// Write the given rows to this SheetWriter
func (sw *SheetWriter) WriteRows(rows []Row) error {
	if sw.closed {
		panic("Can not write to closed SheetWriter")
	}

	var err error

	for i, r := range rows {
		rb := &bytes.Buffer{}

		if sw.maxNCols < uint64(len(r.Cells)) {
			sw.maxNCols = uint64(len(r.Cells))
		}

		for j, c := range r.Cells {

			cellX, cellY := CellIndex(uint64(j), uint64(i)+sw.currentIndex)

			if c.Type == CellTypeDatetime {
				d, err := time.Parse(time.RFC3339, c.Value)
				if err == nil {
					c.Value = OADate(d)
				}
			} else if c.Type == CellTypeInlineString {
				c.Value = html.EscapeString(c.Value)
			}

			var cellString string

			switch c.Type {
			case CellTypeString:
				cellString = `<c r="%s%d" t="s" s="1"><v>%s</v></c>`
			case CellTypeInlineString:
				cellString = `<c r="%s%d" t="inlineStr"><is><t>%s</t></is></c>`
			case CellTypeNumber:
				cellString = `<c r="%s%d" t="n" s="1"><v>%s</v></c>`
			case CellTypeDatetime:
				cellString = `<c r="%s%d" s="2"><v>%s</v></c>`
			}

			io.WriteString(rb, fmt.Sprintf(cellString, cellX, cellY, c.Value))

			if err != nil {
				return err
			}
		}

		rowString := fmt.Sprintf(`<row r="%d">%s</row>`, uint64(i)+sw.currentIndex+1, rb.String())

		_, err = io.WriteString(sw.f, rowString)
		if err != nil {
			return err
		}

	}

	sw.currentIndex += uint64(len(rows))

	return nil
}

// Closes the SheetWriter
func (sw *SheetWriter) Close() error {
	if sw.closed {
		panic("SheetWriter already closed")
	}

	cellEndX, cellEndY := CellIndex(sw.maxNCols-1, sw.currentIndex-1)
	sheetEnd := fmt.Sprintf(`<dimension ref="A1:%s%d"/>`, cellEndX, cellEndY)
	sheetEnd += `</sheetData></worksheet>`
	_, err := io.WriteString(sw.f, sheetEnd)

	sw.closed = true

	return err
}

// Writes the header of a sheet
func (sw *SheetWriter) WriteHeader(s *Sheet) error {
	if sw.closed {
		panic("Can not write to closed SheetWriter")
	}

	sheet := struct {
		Cols []Column
	}{
		Cols: s.columns,
	}

	return TemplateSheetStart.Execute(sw.f, sheet)
}
<commit_msg>Work on writing a header in a way that will support multiple sheets<commit_after>// Package implements creation of XLSX simple spreadsheet files

package xlsx

import (
	"archive/zip"
	"bufio"
	"bytes"
	"fmt"
	"html"
	"io"
	"os"
	"strconv"
	"time"
)

type CellType uint

// Basic spreadsheet cell types
const (
	CellTypeNumber CellType = iota
	CellTypeString
	CellTypeDatetime
	CellTypeInlineString
)

// XLSX Spreadsheet Cell
type Cell struct {
	Type  CellType
	Value string
}

// XLSX Spreadsheet Row
type Row struct {
	Cells []Cell
}

// XLSX Spreadsheet Column
type Column struct {
	Name  string
	Width uint64
}

// XLSX Spreadsheet Document Properties
type DocumentInfo struct {
	CreatedBy  string
	ModifiedBy string
	CreatedAt  time.Time
	ModifiedAt time.Time
}

// XLSX Spreadsheet
type Sheet struct {
	Title           string
	columns         []Column
	rows            []Row
	sharedStringMap map[string]int
	sharedStrings   []string
	DocumentInfo    DocumentInfo
}

// Create a sheet with no dimensions
func NewSheet() Sheet {
	c := make([]Column, 0)
	r := make([]Row, 0)
	ssm := make(map[string]int)
	sst := make([]string, 0)

	s := Sheet{
		Title:           "Data",
		columns:         c,
		rows:            r,
		sharedStringMap: ssm,
		sharedStrings:   sst,
	}

	return s
}

// Create a sheet with dimensions derived from the given columns
func NewSheetWithColumns(c []Column) Sheet {
	r := make([]Row, 0)
	ssm := make(map[string]int)
	sst := make([]string, 0)

	s := Sheet{
		Title:           "Data",
		columns:         c,
		rows:            r,
		sharedStringMap: ssm,
		sharedStrings:   sst,
	}

	s.DocumentInfo.CreatedBy = "xlsx.go"
	s.DocumentInfo.CreatedAt = time.Now()

	s.DocumentInfo.ModifiedBy = s.DocumentInfo.CreatedBy
	s.DocumentInfo.ModifiedAt = s.DocumentInfo.CreatedAt

	return s
}

// Create a new row with a length caculated by the sheets known column count
func (s *Sheet) NewRow() Row {
	c := make([]Cell, len(s.columns))
	r := Row{
		Cells: c,
	}
	return r
}

// Append a row to the sheet
func (s *Sheet) AppendRow(r Row) error {
	if len(r.Cells) != len(s.columns) {
		return fmt.Errorf("the given row has %d cells and %d were expected", len(r.Cells), len(s.columns))
	}

	cells := make([]Cell, len(s.columns))

	for n, c := range r.Cells {
		cells[n].Type = c.Type
		cells[n].Value = c.Value

		if cells[n].Type == CellTypeString {
			// calculate string reference
			cells[n].Value = html.EscapeString(cells[n].Value)
			i, exists := s.sharedStringMap[cells[n].Value]
			if !exists {
				i = len(s.sharedStrings)
				s.sharedStringMap[cells[n].Value] = i
				s.sharedStrings = append(s.sharedStrings, cells[n].Value)
			}
			cells[n].Value = strconv.Itoa(i)
		}
	}

	row := s.NewRow()
	row.Cells = cells

	s.rows = append(s.rows, row)

	return nil
}

// Get the Shared Strings in the order they were added to the map
func (s *Sheet) SharedStrings() []string {
	return s.sharedStrings
}

// Given zero-based array indices output the Excel cell reference. For
// example (0,0) => "A1"; (2,2) => "C3"; (26,45) => "AA46"
func CellIndex(x, y uint64) (string, uint64) {
	return colName(x), (y + 1)
}

// From a zero-based column number return the Excel column name.
// For example: 0 => "A"; 2 => "C"; 26 => "AA"
func colName(n uint64) string {
	var s string
	n += 1

	for n > 0 {
		n -= 1
		s = string(65+(n%26)) + s
		n /= 26
	}

	return s
}

// Convert time to the OLE Automation format.
func OADate(d time.Time) string {
	epoch := time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
	nsPerDay := 24 * time.Hour

	v := -1 * float64(epoch.Sub(d)) / float64(nsPerDay)

	// TODO: deal with dates before epoch
	// e.g. http://stackoverflow.com/questions/15549823/oadate-to-milliseconds-timestamp-in-javascript/15550284#15550284

	if d.Hour() == 0 && d.Minute() == 0 && d.Second() == 0 {
		return fmt.Sprintf("%d", int64(v))
	} else {
		return fmt.Sprintf("%f", v)
	}
}

// Create filename and save the XLSX file
func (s *Sheet) SaveToFile(filename string) error {
	outputfile, err := os.Create(filename)
	if err != nil {
		return err
	}
	w := bufio.NewWriter(outputfile)
	err = s.SaveToWriter(w)
	defer w.Flush()
	return err
}

// Save the XLSX file to the given writer
func (s *Sheet) SaveToWriter(w io.Writer) error {

	ww := NewWorkbookWriter(w)

	sw, err := ww.NewSheetWriter(s)
	if err != nil {
		return err
	}

	err = sw.WriteRows(s.rows)
	if err != nil {
		return err
	}

	err = ww.Close()

	return err
}

// Handles the writing of an XLSX workbook
type WorkbookWriter struct {
	zipWriter     *zip.Writer
	sheetWriter   *SheetWriter
	headerWritten bool
	closed        bool
}

// NewWorkbookWriter creates a new WorkbookWriter, which SheetWriters will
// operate on. It must be closed when all Sheets have been written.
func NewWorkbookWriter(w io.Writer) *WorkbookWriter {
	return &WorkbookWriter{zip.NewWriter(w), nil, false, false}
}

// Write the header files of the workbook
func (ww *WorkbookWriter) WriteHeader(s ...*Sheet) error {
	if ww.headerWritten {
		panic("Workbook header already written")
	}

	z := ww.zipWriter

	f, err := z.Create("[Content_Types].xml")
	err = TemplateContentTypes.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("docProps/app.xml")
	err = TemplateApp.Execute(os.Stdout, s[0])
	err = TemplateApp.Execute(f, s[0])
	if err != nil {
		return err
	}

	f, err = z.Create("docProps/core.xml")
	err = TemplateCore.Execute(f, s[0].DocumentInfo)
	if err != nil {
		return err
	}

	f, err = z.Create("_rels/.rels")
	err = TemplateRelationships.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/workbook.xml")
	err = TemplateWorkbook.Execute(f, s[0])
	if err != nil {
		return err
	}

	f, err = z.Create("xl/_rels/workbook.xml.rels")
	err = TemplateWorkbookRelationships.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/styles.xml")
	err = TemplateStyles.Execute(f, nil)
	if err != nil {
		return err
	}

	f, err = z.Create("xl/sharedStrings.xml")
	err = TemplateStringLookups.Execute(f, s[0].SharedStrings())
	if err != nil {
		return err
	}

	return nil
}

// Closes the WorkbookWriter
func (ww *WorkbookWriter) Close() error {
	if ww.closed {
		panic("WorkbookWriter already closed")
	}

	if ww.sheetWriter != nil {
		err := ww.sheetWriter.Close()
		if err != nil {
			return err
		}
	}

	ww.closed = true

	return ww.zipWriter.Close()
}

// NewSheetWriter creates a new SheetWriter in this workbook using the given sheet.
// It returns a SheetWriter to which rows can be written.
// All rows must be written to the SheetWriter before the next call to NewSheetWriter,
// as this will automatically close the previous SheetWriter.
func (ww *WorkbookWriter) NewSheetWriter(s *Sheet) (*SheetWriter, error) {
	if ww.closed {
		panic("Can not write to closed WorkbookWriter")
	}

	if !ww.headerWritten {
		err := ww.WriteHeader(s)
		if err != nil {
			return nil, err
		}
	}

	f, err := ww.zipWriter.Create("xl/worksheets/" + "sheet1" + ".xml")
	sw := &SheetWriter{f, err, 0, 0, false}

	if ww.sheetWriter != nil {
		err = ww.sheetWriter.Close()
		if err != nil {
			return nil, err
		}
	}

	ww.sheetWriter = sw
	err = sw.WriteHeader(s)

	return sw, err
}

// Handles the writing of a sheet
type SheetWriter struct {
	f            io.Writer
	err          error
	currentIndex uint64
	maxNCols     uint64
	closed       bool
}

// Write the given rows to this SheetWriter
func (sw *SheetWriter) WriteRows(rows []Row) error {
	if sw.closed {
		panic("Can not write to closed SheetWriter")
	}

	var err error

	for i, r := range rows {
		rb := &bytes.Buffer{}

		if sw.maxNCols < uint64(len(r.Cells)) {
			sw.maxNCols = uint64(len(r.Cells))
		}

		for j, c := range r.Cells {

			cellX, cellY := CellIndex(uint64(j), uint64(i)+sw.currentIndex)

			if c.Type == CellTypeDatetime {
				d, err := time.Parse(time.RFC3339, c.Value)
				if err == nil {
					c.Value = OADate(d)
				}
			} else if c.Type == CellTypeInlineString {
				c.Value = html.EscapeString(c.Value)
			}

			var cellString string

			switch c.Type {
			case CellTypeString:
				cellString = `<c r="%s%d" t="s" s="1"><v>%s</v></c>`
			case CellTypeInlineString:
				cellString = `<c r="%s%d" t="inlineStr"><is><t>%s</t></is></c>`
			case CellTypeNumber:
				cellString = `<c r="%s%d" t="n" s="1"><v>%s</v></c>`
			case CellTypeDatetime:
				cellString = `<c r="%s%d" s="2"><v>%s</v></c>`
			}

			io.WriteString(rb, fmt.Sprintf(cellString, cellX, cellY, c.Value))

			if err != nil {
				return err
			}
		}

		rowString := fmt.Sprintf(`<row r="%d">%s</row>`, uint64(i)+sw.currentIndex+1, rb.String())

		_, err = io.WriteString(sw.f, rowString)
		if err != nil {
			return err
		}

	}

	sw.currentIndex += uint64(len(rows))

	return nil
}

// Closes the SheetWriter
func (sw *SheetWriter) Close() error {
	if sw.closed {
		panic("SheetWriter already closed")
	}

	cellEndX, cellEndY := CellIndex(sw.maxNCols-1, sw.currentIndex-1)
	sheetEnd := fmt.Sprintf(`<dimension ref="A1:%s%d"/>`, cellEndX, cellEndY)
	sheetEnd += `</sheetData></worksheet>`
	_, err := io.WriteString(sw.f, sheetEnd)

	sw.closed = true

	return err
}

// Writes the header of a sheet
func (sw *SheetWriter) WriteHeader(s *Sheet) error {
	if sw.closed {
		panic("Can not write to closed SheetWriter")
	}

	sheet := struct {
		Cols []Column
	}{
		Cols: s.columns,
	}

	return TemplateSheetStart.Execute(sw.f, sheet)
}
<|endoftext|>


<commit_before>// Gone Time Tracker -or- Where has my time gone?
package main

import (
	"bytes"
	"errors"
	"log"
	"os"
	"time"

	"github.com/BurntSushi/xgb"
	"github.com/BurntSushi/xgb/screensaver"
	"github.com/BurntSushi/xgb/xproto"
)

type Xorg struct {
	conn        *xgb.Conn
	root        xproto.Window
	activeAtom  *xproto.InternAtomReply
	netNameAtom *xproto.InternAtomReply
	nameAtom    *xproto.InternAtomReply
	classAtom   *xproto.InternAtomReply
	event       chan xgb.Event
}

func (x Xorg) atom(aname string) *xproto.InternAtomReply {
	a, err := xproto.InternAtom(x.conn, true,
		uint16(len(aname)), aname).Reply()
	if err != nil {
		log.Fatal("atom: ", err)
	}
	return a
}

func (x Xorg) property(w xproto.Window,
	a *xproto.InternAtomReply) (*xproto.GetPropertyReply, error) {
	return xproto.GetProperty(x.conn, false, w, a.Atom,
		xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
}

func (x Xorg) active() xproto.Window {
	p, err := x.property(x.root, x.activeAtom)
	if err != nil {
		return x.root
	}
	return xproto.Window(xgb.Get32(p.Value))
}

func (x Xorg) name(w xproto.Window) (string, error) {
	name, err := x.property(w, x.netNameAtom)
	if err != nil {
		return "", err
	}
	if string(name.Value) == "" {
		name, err = x.property(w, x.nameAtom)
		if err != nil {
			return "", err
		}
		if string(name.Value) == "" {
			return "", errors.New("empty value")
		}
	}
	return string(name.Value), nil
}

func (x Xorg) class(w xproto.Window) (string, error) {
	class, err := x.property(w, x.classAtom)
	if err != nil {
		return "", err
	}
	zero := []byte{0}
	s := bytes.Split(bytes.TrimSuffix(class.Value, zero), zero)
	if l := len(s); l > 0 && len(s[l-1]) != 0 {
		return string(s[l-1]), nil
	}
	return "", errors.New("empty class")
}

func (x Xorg) window() (Window, bool) {
	id := x.active()
	/* skip invalid window id */
	if id == 0 {
		return Window{}, false
	}
	class, err := x.class(id)
	if err != nil {
		return Window{}, false
	}
	name, err := x.name(id)
	if err != nil {
		return Window{}, false
	}
	x.spy(id)
	return Window{
		Class: class,
		Name:  name,
	}, true
}

func (x Xorg) spy(w xproto.Window) {
	xproto.ChangeWindowAttributes(x.conn, w, xproto.CwEventMask,
		[]uint32{xproto.EventMaskPropertyChange})
}

func (x Xorg) Close() {
	x.conn.Close()
}

func Connect() Xorg {
	var x Xorg
	var err error

	display := os.Getenv("DISPLAY")
	if display == "" {
		display = ":0"
	}
	x.conn, err = xgb.NewConnDisplay(display)
	if err != nil {
		log.Fatal("xgb: ", err)
	}

	err = screensaver.Init(x.conn)
	if err != nil {
		log.Fatal("screensaver: ", err)
	}

	setup := xproto.Setup(x.conn)
	x.root = setup.DefaultScreen(x.conn).Root

	drw := xproto.Drawable(x.root)
	screensaver.SelectInput(x.conn, drw, screensaver.EventNotifyMask)

	x.activeAtom = x.atom("_NET_ACTIVE_WINDOW")
	x.netNameAtom = x.atom("_NET_WM_NAME")
	x.nameAtom = x.atom("WM_NAME")
	x.classAtom = x.atom("WM_CLASS")
	x.event = make(chan xgb.Event)

	x.spy(x.root)

	return x
}

func (x Xorg) waitForEvent() {
	for {
		ev, err := x.conn.WaitForEvent()
		if err != nil {
			log.Println("wait for event:", err)
		}
		x.event <- ev
	}
}

func (x Xorg) queryIdle() time.Duration {
	info, err := screensaver.QueryInfo(x.conn,
		xproto.Drawable(x.root)).Reply()
	if err != nil {
		log.Println("query idle:", err)
		return 0
	}
	return time.Duration(info.MsSinceUserInput) * time.Millisecond
}

func (x Xorg) Collect(t Tracker) {
	if win, ok := x.window(); ok {
		t.Update(win)
	}

	go x.waitForEvent()

	for {
		select {
		case event := <-x.event:
			switch e := event.(type) {
			case xproto.PropertyNotifyEvent:
				if win, ok := x.window(); ok {
					t.Wakeup()
					t.Update(win)
				}
			case screensaver.NotifyEvent:
				switch e.State {
				case screensaver.StateOn:
					idle := x.queryIdle()
					t.Snooze(idle)
				default:
					t.Wakeup()
				}
			}
		case <-time.After(time.Minute):
			idle := x.queryIdle()
			if idle > 5*time.Minute {
				t.Snooze(idle)
			}
		}
	}
}
<commit_msg>limit event chan to 1<commit_after>// Gone Time Tracker -or- Where has my time gone?
package main

import (
	"bytes"
	"errors"
	"log"
	"os"
	"time"

	"github.com/BurntSushi/xgb"
	"github.com/BurntSushi/xgb/screensaver"
	"github.com/BurntSushi/xgb/xproto"
)

type Xorg struct {
	conn        *xgb.Conn
	root        xproto.Window
	activeAtom  *xproto.InternAtomReply
	netNameAtom *xproto.InternAtomReply
	nameAtom    *xproto.InternAtomReply
	classAtom   *xproto.InternAtomReply
	event       chan xgb.Event
}

func (x Xorg) atom(aname string) *xproto.InternAtomReply {
	a, err := xproto.InternAtom(x.conn, true,
		uint16(len(aname)), aname).Reply()
	if err != nil {
		log.Fatal("atom: ", err)
	}
	return a
}

func (x Xorg) property(w xproto.Window,
	a *xproto.InternAtomReply) (*xproto.GetPropertyReply, error) {
	return xproto.GetProperty(x.conn, false, w, a.Atom,
		xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
}

func (x Xorg) active() xproto.Window {
	p, err := x.property(x.root, x.activeAtom)
	if err != nil {
		return x.root
	}
	return xproto.Window(xgb.Get32(p.Value))
}

func (x Xorg) name(w xproto.Window) (string, error) {
	name, err := x.property(w, x.netNameAtom)
	if err != nil {
		return "", err
	}
	if string(name.Value) == "" {
		name, err = x.property(w, x.nameAtom)
		if err != nil {
			return "", err
		}
		if string(name.Value) == "" {
			return "", errors.New("empty value")
		}
	}
	return string(name.Value), nil
}

func (x Xorg) class(w xproto.Window) (string, error) {
	class, err := x.property(w, x.classAtom)
	if err != nil {
		return "", err
	}
	zero := []byte{0}
	s := bytes.Split(bytes.TrimSuffix(class.Value, zero), zero)
	if l := len(s); l > 0 && len(s[l-1]) != 0 {
		return string(s[l-1]), nil
	}
	return "", errors.New("empty class")
}

func (x Xorg) window() (Window, bool) {
	id := x.active()
	/* skip invalid window id */
	if id == 0 {
		return Window{}, false
	}
	class, err := x.class(id)
	if err != nil {
		return Window{}, false
	}
	name, err := x.name(id)
	if err != nil {
		return Window{}, false
	}
	x.spy(id)
	return Window{
		Class: class,
		Name:  name,
	}, true
}

func (x Xorg) spy(w xproto.Window) {
	xproto.ChangeWindowAttributes(x.conn, w, xproto.CwEventMask,
		[]uint32{xproto.EventMaskPropertyChange})
}

func (x Xorg) Close() {
	x.conn.Close()
}

func Connect() Xorg {
	var x Xorg
	var err error

	display := os.Getenv("DISPLAY")
	if display == "" {
		display = ":0"
	}
	x.conn, err = xgb.NewConnDisplay(display)
	if err != nil {
		log.Fatal("xgb: ", err)
	}

	err = screensaver.Init(x.conn)
	if err != nil {
		log.Fatal("screensaver: ", err)
	}

	setup := xproto.Setup(x.conn)
	x.root = setup.DefaultScreen(x.conn).Root

	drw := xproto.Drawable(x.root)
	screensaver.SelectInput(x.conn, drw, screensaver.EventNotifyMask)

	x.activeAtom = x.atom("_NET_ACTIVE_WINDOW")
	x.netNameAtom = x.atom("_NET_WM_NAME")
	x.nameAtom = x.atom("WM_NAME")
	x.classAtom = x.atom("WM_CLASS")
	x.event = make(chan xgb.Event, 1)

	x.spy(x.root)

	return x
}

func (x Xorg) waitForEvent() {
	for {
		ev, err := x.conn.WaitForEvent()
		if err != nil {
			log.Println("wait for event:", err)
		}
		x.event <- ev
	}
}

func (x Xorg) queryIdle() time.Duration {
	info, err := screensaver.QueryInfo(x.conn,
		xproto.Drawable(x.root)).Reply()
	if err != nil {
		log.Println("query idle:", err)
		return 0
	}
	return time.Duration(info.MsSinceUserInput) * time.Millisecond
}

func (x Xorg) Collect(t Tracker) {
	if win, ok := x.window(); ok {
		t.Update(win)
	}

	go x.waitForEvent()

	for {
		select {
		case event := <-x.event:
			switch e := event.(type) {
			case xproto.PropertyNotifyEvent:
				if win, ok := x.window(); ok {
					t.Wakeup()
					t.Update(win)
				}
			case screensaver.NotifyEvent:
				switch e.State {
				case screensaver.StateOn:
					idle := x.queryIdle()
					t.Snooze(idle)
				default:
					t.Wakeup()
				}
			}
		case <-time.After(time.Minute):
			idle := x.queryIdle()
			if idle > 5*time.Minute {
				t.Snooze(idle)
			}
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2013 Alex Browne.  All rights reserved.
// Use of this source code is governed by the MIT
// license, which can be found in the LICENSE file.

// File zoom.go contains glue code that connects the Model
// interface to the database. The most basic
// public-facing methods are here.

// Package zoom is a lightweight, blazing-fast ORM powered by Redis.
package zoom

import (
	"errors"
	"fmt"
	"reflect"
)

// Save writes a model (a struct which satisfies the Model interface) to the redis
// database. Save throws an error if the type of the struct has not yet been registered
// or if there is a problem connecting to the database. If the Id field of the struct is
// empty, Save will mutate the struct by setting the Id. To make a struct satisfy the Model
// interface, you can embed zoom.DefaultData.
func Save(model Model) error {
	t := newTransaction()

	// add a save operation to the transaction
	if err := t.saveModel(model); err != nil {
		return err
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MSave is like Save but accepts a slice of models and saves them all in
// a single transaction. See http://redis.io/topics/transactions. If there
// is an error in the middle of the transaction, any models that were saved
// before the error was encountered will still be saved. Usually this is fine
// because saving a model a second time will have no adverse effects.
func MSave(models []Model) error {
	t := newTransaction()

	// add a save operation for each model to the transaction
	for _, m := range models {
		if err := t.saveModel(m); err != nil {
			return err
		}
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// FindById gets a model from the database. It returns an error
// if a model with that id does not exist or if there was a problem
// connecting to the database. By default modelName should be the
// string version of the type of model (without the asterisk, ampersand,
// or package prefix). If you used RegisterName instead of Register,
// modelName should be the custom name you used.
func FindById(modelName, id string) (Model, error) {

	// create a new struct of proper type
	typ, err := getRegisteredTypeFromName(modelName)
	if err != nil {
		return nil, err
	}
	val := reflect.New(typ.Elem())
	m, ok := val.Interface().(Model)
	if !ok {
		msg := fmt.Sprintf("zoom: could not convert val of type %T to Model\n", val.Interface())
		return nil, errors.New(msg)
	}

	// invoke ScanById
	if err := ScanById(id, m); err != nil {
		return m, err
	}
	return m, nil
}

// MFindById is like FindById but accepts a slice of model names and ids
// and returns a slice of models. It executes the commands needed to retrieve
// the models in a single transaction. See http://redis.io/topics/transactions.
// The slice of modelNames and ids should be properly aligned so that, e.g.,
// modelNames[0] corresponds to ids[0]. If there is an error in the middle of
// the transaction, the function will halt and return the models retrieved so
// far (as well as the error).
func MFindById(modelNames, ids []string) ([]Model, error) {

	if len(modelNames) != len(ids) {
		return nil, errors.New("Zoom: error in MFindById: modelNames and ids must be the same length")
	}

	t := newTransaction()
	results := make([]Model, 0)

	for i := 0; i < len(modelNames); i++ {
		name, id := modelNames[i], ids[i]

		// create a new struct of proper type
		typ, err := getRegisteredTypeFromName(name)
		if err != nil {
			return results, err
		}
		val := reflect.New(typ.Elem())
		m, ok := val.Interface().(Model)
		if !ok {
			msg := fmt.Sprintf("zoom: could not convert val of type %T to Model\n", val.Interface())
			return results, errors.New(msg)
		}
		results = append(results, m)

		// create a modelRef
		mr, err := newModelRefFromModel(m)
		if err != nil {
			return results, err
		}
		mr.model.setId(id)

		// add a find operation to the transaction
		t.findModel(mr, nil)
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return results, err
	}
	return results, nil
}

// ScanById retrieves a model from redis and scans it into model.
// model should be a pointer to a struct of a registered type. ScanById
// will mutate the struct, filling in its fields. It returns an error
// if a model with that id does not exist or if there was a problem
// connecting to the database.
func ScanById(id string, model Model) error {

	// create a modelRef
	mr, err := newModelRefFromModel(model)
	if err != nil {
		return err
	}
	mr.model.setId(id)

	// start a transaction
	t := newTransaction()
	t.findModel(mr, nil)

	// execute the transaction and return the result
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MScanById is like ScanById but accepts a slice of ids and a pointer to
// a slice of models. It executes the commands needed to retrieve the models
// in a single transaction. See http://redis.io/topics/transactions.
// The slice of ids and models should be properly aligned so that, e.g.,
// ids[0] corresponds to models[0]. If there is an error in the middle of the
// transaction, the function will halt and return the error. Any models that
// were scanned before the error will still be valid. If any of the models in
// the models slice are nil, MScanById will use reflection to allocate memory
// for them.
func MScanById(ids []string, models interface{}) error {

	// since this is somewhat type-unsafe, we need to verify that
	// models is the correct type
	modelsVal := reflect.ValueOf(models).Elem()
	modelType := modelsVal.Type().Elem()

	if len(ids) != modelsVal.Len() {
		return errors.New("Zoom: error in MScanById: ids and models must be the same length")
	} else if !typeIsSliceOrArray(modelsVal.Type()) {
		return errors.New("Zoom: error in MScanById: models should be a pointer to a slice or array of models (models are pointers to structs)")
	} else if !typeIsPointerToStruct(modelType) {
		return errors.New("Zoom: error in MScanById: models should be a pointer to a slice of pointers to structs")
	} else if !modelTypeIsRegistered(modelType) {
		msg := fmt.Sprintf("Zoom: error in MScanById: the elements in models should be of a registered type\nType %s has not been registered.", modelType.String())
		return errors.New(msg)
	}

	t := newTransaction()
	for i := 0; i < len(ids); i++ {
		id, mVal := ids[i], modelsVal.Index(i)

		if mVal.IsNil() {
			mVal.Set(reflect.New(modelType.Elem()))
		}

		// create a modelRef
		mr, err := newModelRefFromInterface(mVal.Interface())
		if err != nil {
			return err
		}
		mr.model.setId(id)

		// start a transaction
		t.findModel(mr, nil)
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// Delete removes a model from the database. It will throw an error if
// the type of the model has not yet been registered, if the Id field
// of the model is empty, or if there is a problem connecting to the
// database. If the model does not exist in the database, Delete will
// not return an error; it will simply have no effect.
func Delete(model Model) error {
	t := newTransaction()

	if model.getId() == "" {
		return errors.New("zoom: cannot delete because model Id field is empty")
	}
	mr, err := newModelRefFromModel(model)
	if err != nil {
		return err
	}
	if err := t.deleteModel(mr); err != nil {
		return err
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MDelete is like Delete but accepts a slice of models and
// deletes them all in a single transaction. See
// http://redis.io/topics/transactions. If an error is encountered
// in the middle of the transaction, the function will halt and return
// the error. In that case, any models which were deleted before the
// error was encountered will still be deleted. Usually this is fine
// because calling Delete on a model a second time will have no adverse
// effects.
func MDelete(models []Model) error {
	t := newTransaction()
	for _, m := range models {
		if m.getId() == "" {
			return errors.New("zoom: cannot delete because model Id field is empty")
		}
		mr, err := newModelRefFromModel(m)
		if err != nil {
			return err
		}
		if err := t.deleteModel(mr); err != nil {
			return err
		}
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// DeleteById removes a model from the database by its registered name and id.
// By default modelName should be the string version of the type of model (without
// the asterisk, ampersand, or package prefix). If you used RegisterName instead of
// Register, modelName should be the custom name you used. DeleteById will throw an error
// if modelName is invalid or if there is a problem connecting to the database. If
// the model does not exist, DeleteById will not return an error; it will simply have
// no effect.
func DeleteById(modelName string, id string) error {
	t := newTransaction()
	if err := t.deleteModelById(modelName, id); err != nil {
		return err
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MDeleteById is like DeleteById but accepts a slice of modelNames and ids
// and deletes them all in a single transaction. See http://redis.io/topics/transactions.
// The slice of modelNames and ids should be properly aligned so that, e.g.,
// modelNames[0] corresponds to ids[0]. If there is an error in the middle of
// the transaction, the function will halt and return the error. In that case,
// any models which were deleted before the error was encountered will still be
// deleted. Usually this is fine because calling Delete on a model a second time
// will have no adverse effects.
func MDeleteById(modelNames []string, ids []string) error {
	if len(modelNames) != len(ids) {
		return errors.New("Zoom: error in MDeleteById: modelNames and ids must be the same length")
	}

	t := newTransaction()
	for i := 0; i < len(modelNames); i++ {
		name, id := modelNames[i], ids[i]
		if err := t.deleteModelById(name, id); err != nil {
			return err
		}
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}
<commit_msg>Fix some ambiguous error messages in MScanById<commit_after>// Copyright 2013 Alex Browne.  All rights reserved.
// Use of this source code is governed by the MIT
// license, which can be found in the LICENSE file.

// File zoom.go contains glue code that connects the Model
// interface to the database. The most basic
// public-facing methods are here.

// Package zoom is a lightweight, blazing-fast ORM powered by Redis.
package zoom

import (
	"errors"
	"fmt"
	"reflect"
)

// Save writes a model (a struct which satisfies the Model interface) to the redis
// database. Save throws an error if the type of the struct has not yet been registered
// or if there is a problem connecting to the database. If the Id field of the struct is
// empty, Save will mutate the struct by setting the Id. To make a struct satisfy the Model
// interface, you can embed zoom.DefaultData.
func Save(model Model) error {
	t := newTransaction()

	// add a save operation to the transaction
	if err := t.saveModel(model); err != nil {
		return err
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MSave is like Save but accepts a slice of models and saves them all in
// a single transaction. See http://redis.io/topics/transactions. If there
// is an error in the middle of the transaction, any models that were saved
// before the error was encountered will still be saved. Usually this is fine
// because saving a model a second time will have no adverse effects.
func MSave(models []Model) error {
	t := newTransaction()

	// add a save operation for each model to the transaction
	for _, m := range models {
		if err := t.saveModel(m); err != nil {
			return err
		}
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// FindById gets a model from the database. It returns an error
// if a model with that id does not exist or if there was a problem
// connecting to the database. By default modelName should be the
// string version of the type of model (without the asterisk, ampersand,
// or package prefix). If you used RegisterName instead of Register,
// modelName should be the custom name you used.
func FindById(modelName, id string) (Model, error) {

	// create a new struct of proper type
	typ, err := getRegisteredTypeFromName(modelName)
	if err != nil {
		return nil, err
	}
	val := reflect.New(typ.Elem())
	m, ok := val.Interface().(Model)
	if !ok {
		msg := fmt.Sprintf("zoom: could not convert val of type %T to Model\n", val.Interface())
		return nil, errors.New(msg)
	}

	// invoke ScanById
	if err := ScanById(id, m); err != nil {
		return m, err
	}
	return m, nil
}

// MFindById is like FindById but accepts a slice of model names and ids
// and returns a slice of models. It executes the commands needed to retrieve
// the models in a single transaction. See http://redis.io/topics/transactions.
// The slice of modelNames and ids should be properly aligned so that, e.g.,
// modelNames[0] corresponds to ids[0]. If there is an error in the middle of
// the transaction, the function will halt and return the models retrieved so
// far (as well as the error).
func MFindById(modelNames, ids []string) ([]Model, error) {

	if len(modelNames) != len(ids) {
		return nil, errors.New("Zoom: error in MFindById: modelNames and ids must be the same length")
	}

	t := newTransaction()
	results := make([]Model, 0)

	for i := 0; i < len(modelNames); i++ {
		name, id := modelNames[i], ids[i]

		// create a new struct of proper type
		typ, err := getRegisteredTypeFromName(name)
		if err != nil {
			return results, err
		}
		val := reflect.New(typ.Elem())
		m, ok := val.Interface().(Model)
		if !ok {
			msg := fmt.Sprintf("zoom: could not convert val of type %T to Model\n", val.Interface())
			return results, errors.New(msg)
		}
		results = append(results, m)

		// create a modelRef
		mr, err := newModelRefFromModel(m)
		if err != nil {
			return results, err
		}
		mr.model.setId(id)

		// add a find operation to the transaction
		t.findModel(mr, nil)
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return results, err
	}
	return results, nil
}

// ScanById retrieves a model from redis and scans it into model.
// model should be a pointer to a struct of a registered type. ScanById
// will mutate the struct, filling in its fields. It returns an error
// if a model with that id does not exist or if there was a problem
// connecting to the database.
func ScanById(id string, model Model) error {

	// create a modelRef
	mr, err := newModelRefFromModel(model)
	if err != nil {
		return err
	}
	mr.model.setId(id)

	// start a transaction
	t := newTransaction()
	t.findModel(mr, nil)

	// execute the transaction and return the result
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MScanById is like ScanById but accepts a slice of ids and a pointer to
// a slice of models. It executes the commands needed to retrieve the models
// in a single transaction. See http://redis.io/topics/transactions.
// The slice of ids and models should be properly aligned so that, e.g.,
// ids[0] corresponds to models[0]. If there is an error in the middle of the
// transaction, the function will halt and return the error. Any models that
// were scanned before the error will still be valid. If any of the models in
// the models slice are nil, MScanById will use reflection to allocate memory
// for them.
func MScanById(ids []string, models interface{}) error {

	// since this is somewhat type-unsafe, we need to verify that
	// models is the correct type
	modelsVal := reflect.ValueOf(models).Elem()
	modelType := modelsVal.Type().Elem()

	if len(ids) != modelsVal.Len() {
		return errors.New("Zoom: error in MScanById: ids and models must be the same length")
	} else if !typeIsSliceOrArray(modelsVal.Type()) {
		return errors.New("Zoom: error in MScanById: models should be a pointer to a slice or array of models")
	} else if !typeIsPointerToStruct(modelType) {
		return errors.New("Zoom: error in MScanById: the elements in models should be pointers to structs")
	} else if !modelTypeIsRegistered(modelType) {
		msg := fmt.Sprintf("Zoom: error in MScanById: the elements in models should be of a registered type\nType %s has not been registered.", modelType.String())
		return errors.New(msg)
	}

	t := newTransaction()
	for i := 0; i < len(ids); i++ {
		id, mVal := ids[i], modelsVal.Index(i)

		if mVal.IsNil() {
			mVal.Set(reflect.New(modelType.Elem()))
		}

		// create a modelRef
		mr, err := newModelRefFromInterface(mVal.Interface())
		if err != nil {
			return err
		}
		mr.model.setId(id)

		// start a transaction
		t.findModel(mr, nil)
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// Delete removes a model from the database. It will throw an error if
// the type of the model has not yet been registered, if the Id field
// of the model is empty, or if there is a problem connecting to the
// database. If the model does not exist in the database, Delete will
// not return an error; it will simply have no effect.
func Delete(model Model) error {
	t := newTransaction()

	if model.getId() == "" {
		return errors.New("zoom: cannot delete because model Id field is empty")
	}
	mr, err := newModelRefFromModel(model)
	if err != nil {
		return err
	}
	if err := t.deleteModel(mr); err != nil {
		return err
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MDelete is like Delete but accepts a slice of models and
// deletes them all in a single transaction. See
// http://redis.io/topics/transactions. If an error is encountered
// in the middle of the transaction, the function will halt and return
// the error. In that case, any models which were deleted before the
// error was encountered will still be deleted. Usually this is fine
// because calling Delete on a model a second time will have no adverse
// effects.
func MDelete(models []Model) error {
	t := newTransaction()
	for _, m := range models {
		if m.getId() == "" {
			return errors.New("zoom: cannot delete because model Id field is empty")
		}
		mr, err := newModelRefFromModel(m)
		if err != nil {
			return err
		}
		if err := t.deleteModel(mr); err != nil {
			return err
		}
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// DeleteById removes a model from the database by its registered name and id.
// By default modelName should be the string version of the type of model (without
// the asterisk, ampersand, or package prefix). If you used RegisterName instead of
// Register, modelName should be the custom name you used. DeleteById will throw an error
// if modelName is invalid or if there is a problem connecting to the database. If
// the model does not exist, DeleteById will not return an error; it will simply have
// no effect.
func DeleteById(modelName string, id string) error {
	t := newTransaction()
	if err := t.deleteModelById(modelName, id); err != nil {
		return err
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}

// MDeleteById is like DeleteById but accepts a slice of modelNames and ids
// and deletes them all in a single transaction. See http://redis.io/topics/transactions.
// The slice of modelNames and ids should be properly aligned so that, e.g.,
// modelNames[0] corresponds to ids[0]. If there is an error in the middle of
// the transaction, the function will halt and return the error. In that case,
// any models which were deleted before the error was encountered will still be
// deleted. Usually this is fine because calling Delete on a model a second time
// will have no adverse effects.
func MDeleteById(modelNames []string, ids []string) error {
	if len(modelNames) != len(ids) {
		return errors.New("Zoom: error in MDeleteById: modelNames and ids must be the same length")
	}

	t := newTransaction()
	for i := 0; i < len(modelNames); i++ {
		name, id := modelNames[i], ids[i]
		if err := t.deleteModelById(name, id); err != nil {
			return err
		}
	}

	// execute the transaction
	if err := t.exec(); err != nil {
		return err
	}
	return nil
}
<|endoftext|>


<commit_before>/*
Copyright (C) 2017 Ignasi Barrera

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"time"

	"github.com/gorilla/mux"
	mp3 "github.com/hajimehoshi/go-mp3"
	"github.com/hajimehoshi/oto"
	rpio "github.com/stianeikeland/go-rpio"
)

// Zuul is the Abiquo Gatekeeper that will take care of opening the door and welcoming
// all the people that comes into the office
type Zuul struct {
	WelcomeFile string
	PinNumber   int
	pin         rpio.Pin
}

// Init initializes the Zuul API and configures the HTTP routes for its commands
func (z *Zuul) Init() (*mux.Router, error) {
	if _, err := os.Stat(z.WelcomeFile); err != nil {
		return nil, fmt.Errorf("could not open welcome audio file: %v", err)
	}
	if err := rpio.Open(); err != nil {
		return nil, fmt.Errorf("error initializing GPIO system: %v", err)
	}

	// Initialize GPIO pin
	z.pin = rpio.Pin(z.PinNumber)
	z.pin.Output()

	// Configure APi routes
	router := mux.NewRouter()
	router.HandleFunc("/puerta", z.Puerta)
	router.HandleFunc("/palante", z.PlayWelcomeFile)
	router.HandleFunc("/dimelo", z.Say).Methods("POST")
	return router, nil
}

// Puerta handles the requests to open the door and configures the GPIO pin accordingly
func (z *Zuul) Puerta(w http.ResponseWriter, r *http.Request) {
	log.Println("Received door open request")
	z.pin.Low()
	time.Sleep(500 * time.Millisecond)
	z.pin.High()
}

// PlayWelcomeFile plays the welcome file in the speakers connected to the Raspberry Pi
func (z *Zuul) PlayWelcomeFile(w http.ResponseWriter, r *http.Request) {
	log.Println("Received welcome audio play request")
	f, err := os.Open(z.WelcomeFile)
	if err != nil {
		log.Printf("Error opening audio file: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
	defer f.Close()

	d, err := mp3.NewDecoder(f)
	if err != nil {
		log.Printf("Error decoding audio file: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
	defer d.Close()

	p, err := oto.NewPlayer(d.SampleRate(), 2, 2, 8192)
	if err != nil {
		log.Printf("Error initializing audio player: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
	defer p.Close()

	if _, err := io.Copy(p, d); err != nil {
		log.Printf("Error playing audio file: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
}

// Say plays the given audio text using the configured text to speech tool
func (z *Zuul) Say(w http.ResponseWriter, r *http.Request) {
	text := r.PostFormValue("text")
	log.Printf("Received text to speak: %v", text)
	cmd := exec.Command("espeak", "-ves+f4", "-s150", fmt.Sprintf("\"%v\"", text))
	if err := cmd.Run(); err != nil {
		log.Printf("Error runnint text-to-peech command: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
}
<commit_msg>Reset the pin on startup<commit_after>/*
Copyright (C) 2017 Ignasi Barrera

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"time"

	"github.com/gorilla/mux"
	mp3 "github.com/hajimehoshi/go-mp3"
	"github.com/hajimehoshi/oto"
	rpio "github.com/stianeikeland/go-rpio"
)

// Zuul is the Abiquo Gatekeeper that will take care of opening the door and welcoming
// all the people that comes into the office
type Zuul struct {
	WelcomeFile string
	PinNumber   int
	pin         rpio.Pin
}

// Init initializes the Zuul API and configures the HTTP routes for its commands
func (z *Zuul) Init() (*mux.Router, error) {
	if _, err := os.Stat(z.WelcomeFile); err != nil {
		return nil, fmt.Errorf("could not open welcome audio file: %v", err)
	}
	if err := rpio.Open(); err != nil {
		return nil, fmt.Errorf("error initializing GPIO system: %v", err)
	}

	// Initialize GPIO pin
	z.pin = rpio.Pin(z.PinNumber)
	z.pin.Output()
	z.pin.High() // Make sure the door is closed when starting

	// Configure APi routes
	router := mux.NewRouter()
	router.HandleFunc("/puerta", z.Puerta)
	router.HandleFunc("/palante", z.PlayWelcomeFile)
	router.HandleFunc("/dimelo", z.Say).Methods("POST")
	return router, nil
}

// Puerta handles the requests to open the door and configures the GPIO pin accordingly
func (z *Zuul) Puerta(w http.ResponseWriter, r *http.Request) {
	log.Println("Received door open request")
	z.pin.Low()
	time.Sleep(500 * time.Millisecond)
	z.pin.High()
}

// PlayWelcomeFile plays the welcome file in the speakers connected to the Raspberry Pi
func (z *Zuul) PlayWelcomeFile(w http.ResponseWriter, r *http.Request) {
	log.Println("Received welcome audio play request")
	f, err := os.Open(z.WelcomeFile)
	if err != nil {
		log.Printf("Error opening audio file: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
	defer f.Close()

	d, err := mp3.NewDecoder(f)
	if err != nil {
		log.Printf("Error decoding audio file: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
	defer d.Close()

	p, err := oto.NewPlayer(d.SampleRate(), 2, 2, 8192)
	if err != nil {
		log.Printf("Error initializing audio player: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
	defer p.Close()

	if _, err := io.Copy(p, d); err != nil {
		log.Printf("Error playing audio file: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
}

// Say plays the given audio text using the configured text to speech tool
func (z *Zuul) Say(w http.ResponseWriter, r *http.Request) {
	text := r.PostFormValue("text")
	log.Printf("Received text to speak: %v", text)
	cmd := exec.Command("espeak", "-ves+f4", "-s150", fmt.Sprintf("\"%v\"", text))
	if err := cmd.Run(); err != nil {
		log.Printf("Error runnint text-to-peech command: %v\n", err)
		http.Error(w, err.Error(), 500)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"path/filepath"

	"fmt"
	"github.com/phrase/phraseapp-api-client/Godeps/_workspace/src/gopkg.in/yaml.v2"
	"github.com/phrase/phraseapp-go/phraseapp"
	"os"
	"path"
)

type Target *PullArgs
type Targets []Target

type PullArgs struct {
	File        string      `yaml:"file,omitempty"`
	ProjectId   string      `yaml:"project_id,omitempty"`
	AccessToken string      `yaml:"access_token,omitempty"`
	Params      *PullParams `yaml:"params,omitempty"`
}

type PullParams struct {
	FileFormat string `yaml:"file_format,omitempty"`
	LocaleId   string `yaml:"locale_id,omitempty"`
}

func PullTargetsFromConfig() (Targets, error) {
	content, err := ConfigContent()
	if err != nil {
		return nil, err
	}

	return parsePull(content)
}

func Pull(p *PhrasePath, target Target) error {
	authenticate()

	locales, err := phraseapp.LocalesList(target.ProjectId, 1, 25)
	if err != nil {
		return err
	}

	paths, err := virtualFilesForPull(p, target, locales)
	if err != nil {
		return err
	}

	localePaths, err := pullFileStrategy(p, paths)
	if err != nil {
		return err
	}

	for _, localePath := range localePaths {
		defaultName := DefaultFileName(p.Mode, localePath.Path)
		if defaultName != "" {
			localePath.Path = path.Join(localePath.Path, p.Separator, defaultName)
		}
		fmt.Println("Create(", localePath.Path, ")")
		err := CreateFile(localePath.Path)
		if err != nil {
			return err
		}
	}

	for _, localePath := range localePaths {

		err := downloadAndWriteToFile(target, localePath)
		if err != nil {
			// fmt.Println(err)
		}
	}

	return nil
}

func downloadAndWriteToFile(target Target, localePath *LocalePath) error {
	downloadParams := new(phraseapp.LocaleDownloadParams)
	downloadParams.FileFormat = target.Params.FileFormat

	res, err := phraseapp.LocaleDownload(target.ProjectId, localePath.LocaleId, downloadParams)
	if err != nil {
		fmt.Println("1", err)
		return err
	}
	fh, err := os.OpenFile(localePath.Path, os.O_WRONLY, 0700)
	if err != nil {
		return err
	}
	defer fh.Close()

	fmt.Println(string(res))
	_, err = fh.Write(res)
	if err != nil {
		fmt.Println("3", err)
		return err
	}
	return nil
}

// locale File handling
func virtualFilesForPull(p *PhrasePath, target Target, locales []*phraseapp.Locale) (LocalePaths, error) {
	switch {
	case p.LocaleTagInPath:
		newFiles, err := FilePathsWithLocales(p, locales)
		if err != nil {
			return nil, err
		}
		return newFiles, nil

	default:
		return expandPathsForSinglePath(p, target, locales)
	}

}

func expandPathsForSinglePath(p *PhrasePath, target Target, locales []*phraseapp.Locale) (LocalePaths, error) {
	absPath, err := filepath.Abs(p.UserPath)
	if err != nil {
		return nil, err
	}

	if target.Params != nil {
		localeId := localeIdForPath(target.Params.LocaleId, locales)
		if localeId == "" {
			return nil, fmt.Errorf("locale specified in your path did not match any remote locales")
		}

		localePath := []*LocalePath{&LocalePath{Path: absPath, LocaleId: localeId}}
		return localePath, nil
	}

	return nil, fmt.Errorf("no target locale id specified")
}

func localeIdForPath(localeId string, locales []*phraseapp.Locale) string {
	for _, locale := range locales {
		if localeId == locale.Id {
			return locale.Id
		}
	}
	return ""
}

// File handling
func pullFileStrategy(p *PhrasePath, paths LocalePaths) (LocalePaths, error) {
	switch {
	case p.Mode == "":
		return paths, nil

	case p.Mode == "*":
		if p.LocaleTagInFile() {
			return singleDirectoryPullStrategy(p, paths)
		} else {
			return paths, nil
		}

	default:
		return paths, nil
	}
}

func singleDirectoryPullStrategy(p *PhrasePath, paths LocalePaths) (LocalePaths, error) {
	extendedPaths := []*LocalePath{}
	for _, localePath := range paths {

		pathsPerDirectory, err := SingleDirectoryStrategy(localePath.Path+"/", "")

		if err != nil {
			return nil, err
		}

		for _, aPath := range pathsPerDirectory {
			localePath := &LocalePath{Path: aPath, LocaleId: localePath.LocaleId}
			extendedPaths = append(extendedPaths, localePath)
		}

	}
	return extendedPaths, nil
}

// Parsing
type PullConfig struct {
	Phraseapp struct {
		AccessToken string `yaml:"access_token"`
		ProjectId   string `yaml:"project_id"`
		Pull        struct {
			Targets Targets
		}
	}
}

func parsePull(yml string) (Targets, error) {
	var config *PullConfig

	err := yaml.Unmarshal([]byte(yml), &config)
	if err != nil {
		return nil, err
	}

	token := config.Phraseapp.AccessToken
	projectId := config.Phraseapp.ProjectId
	targets := config.Phraseapp.Pull.Targets

	for _, target := range targets {
		if target.ProjectId == "" {
			target.ProjectId = projectId
		}
		if target.AccessToken == "" {
			target.AccessToken = token
		}
	}

	return targets, nil
}
<commit_msg>small renamings<commit_after>package main

import (
	"path/filepath"

	"fmt"
	"github.com/phrase/phraseapp-api-client/Godeps/_workspace/src/gopkg.in/yaml.v2"
	"github.com/phrase/phraseapp-go/phraseapp"
	"os"
	"path"
)

type Target *PullArgs
type Targets []Target

type PullArgs struct {
	File        string      `yaml:"file,omitempty"`
	ProjectId   string      `yaml:"project_id,omitempty"`
	AccessToken string      `yaml:"access_token,omitempty"`
	Params      *PullParams `yaml:"params,omitempty"`
}

type PullParams struct {
	FileFormat string `yaml:"file_format,omitempty"`
	LocaleId   string `yaml:"locale_id,omitempty"`
}

func Pull(p *PhrasePath, target Target) error {
	authenticate()

	locales, err := phraseapp.LocalesList(target.ProjectId, 1, 25)
	if err != nil {
		return err
	}

	paths, err := expandPathsWithLocale(p, target, locales)
	if err != nil {
		return err
	}

	virtualPaths, err := fileGlobbingPull(p, paths)
	if err != nil {
		return err
	}

	err = createFiles(p, virtualPaths)
	if err != nil {
		return err
	}

	for _, localePath := range virtualPaths {

		err := downloadAndWriteToFile(target, localePath)
		if err != nil {
			fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
		}
	}

	return nil
}

func PullTargetsFromConfig() (Targets, error) {
	content, err := ConfigContent()
	if err != nil {
		return nil, err
	}

	return parsePull(content)
}

func downloadAndWriteToFile(target Target, localePath *LocalePath) error {
	downloadParams := new(phraseapp.LocaleDownloadParams)
	downloadParams.FileFormat = target.Params.FileFormat

	fmt.Println("Downloading: ", localePath.LocaleId)
	res, err := phraseapp.LocaleDownload(target.ProjectId, localePath.LocaleId, downloadParams)
	if err != nil {
		fmt.Println("1", err)
		return err
	}
	fh, err := os.OpenFile(localePath.Path, os.O_WRONLY, 0700)
	if err != nil {
		return err
	}
	defer fh.Close()

	fmt.Println(string(res))
	_, err = fh.Write(res)
	if err != nil {
		fmt.Println("3", err)
		return err
	}
	return nil
}

// locale File handling
func createFiles(p *PhrasePath, virtualPaths LocalePaths) error {
	for _, localePath := range virtualPaths {
		defaultName := DefaultFileName(p.Mode, localePath.Path)
		if defaultName != "" {
			localePath.Path = path.Join(localePath.Path, p.Separator, defaultName)
		}
		fmt.Println("Creating: ", localePath.Path)
		err := CreateFile(localePath.Path)
		if err != nil {
			return err
		}
	}
	return nil
}

func expandPathsWithLocale(p *PhrasePath, target Target, locales []*phraseapp.Locale) (LocalePaths, error) {
	switch {
	case p.LocaleTagInPath:
		newFiles, err := FilePathsWithLocales(p, locales)
		if err != nil {
			return nil, err
		}
		return newFiles, nil

	default:
		return expandPathWithLocale(p, target, locales)
	}

}

func expandPathWithLocale(p *PhrasePath, target Target, locales []*phraseapp.Locale) (LocalePaths, error) {
	absPath, err := filepath.Abs(p.UserPath)
	if err != nil {
		return nil, err
	}

	if target.Params != nil {
		localeId := localeIdForPath(target.Params.LocaleId, locales)
		if localeId == "" {
			return nil, fmt.Errorf("locale specified in your path did not match any remote locales")
		}

		localePath := []*LocalePath{&LocalePath{Path: absPath, LocaleId: localeId}}
		return localePath, nil
	}

	return nil, fmt.Errorf("no target locale id specified")
}

func localeIdForPath(localeId string, locales []*phraseapp.Locale) string {
	for _, locale := range locales {
		if localeId == locale.Id {
			return locale.Id
		}
	}
	return ""
}

// File handling
func fileGlobbingPull(p *PhrasePath, paths LocalePaths) (LocalePaths, error) {
	switch {
	case p.Mode == "":
		return paths, nil

	case p.Mode == "*":
		if p.LocaleTagInFile() {
			return singleDirectoryPull(p, paths)
		} else {
			return paths, nil
		}

	default:
		return paths, nil
	}
}

func singleDirectoryPull(p *PhrasePath, paths LocalePaths) (LocalePaths, error) {
	extendedPaths := []*LocalePath{}
	for _, localePath := range paths {

		pathsPerDirectory, err := SingleDirectoryStrategy(localePath.Path+"/", "")

		if err != nil {
			return nil, err
		}

		for _, aPath := range pathsPerDirectory {
			localePath := &LocalePath{Path: aPath, LocaleId: localePath.LocaleId}
			extendedPaths = append(extendedPaths, localePath)
		}

	}
	return extendedPaths, nil
}

// Parsing
type PullConfig struct {
	Phraseapp struct {
		AccessToken string `yaml:"access_token"`
		ProjectId   string `yaml:"project_id"`
		Pull        struct {
			Targets Targets
		}
	}
}

func parsePull(yml string) (Targets, error) {
	var config *PullConfig

	err := yaml.Unmarshal([]byte(yml), &config)
	if err != nil {
		return nil, err
	}

	token := config.Phraseapp.AccessToken
	projectId := config.Phraseapp.ProjectId
	targets := config.Phraseapp.Pull.Targets

	for _, target := range targets {
		if target.ProjectId == "" {
			target.ProjectId = projectId
		}
		if target.AccessToken == "" {
			target.AccessToken = token
		}
	}

	return targets, nil
}
<|endoftext|>


<commit_before>package edc

// Rank - struct for rank
type Rank struct {
	ID        int64  `sql:"id" json:"id"`
	Name      string `sql:"name" json:"name"`
	Note      string `sql:"note, null" json:"note"`
	CreatedAt string `sql:"created_at" json:"created_at"`
	UpdatedAt string `sql:"updated_at" json:"updated_at"`
}

// GetRank - get one rank by id
func (e *Edb) GetRank(id int64) (Rank, error) {
	var rank Rank
	if id == 0 {
		return rank, nil
	}
	err := e.db.Model(&rank).
		Where("id = ?", id).
		Select()
	if err != nil {
		errmsg("GetRank select", err)
	}
	return rank, err
}

// GetRankList - get all rank for list
func (e *Edb) GetRankList() ([]Rank, error) {
	var ranks []Rank
	_, err := e.db.Query(&ranks, `
		SELECT
			id,
			name,
			note
		FROM
			ranks
		ORDER BY
			name ASC
	`)
	if err != nil {
		errmsg("GetRankList query", err)
	}
	return ranks, err
}

// GetRankSelect - get all rank for select
func (e *Edb) GetRankSelect() ([]SelectItem, error) {
	var ranks []SelectItem
	_, err := e.db.Query(&ranks, `
		SELECT
			id,
			name
		FROM
			ranks
		ORDER BY
			name ASC
	`)
	if err != nil {
		errmsg("GetRankSelect query", err)
	}
	return ranks, err
}

// CreateRank - create new rank
func (e *Edb) CreateRank(rank Rank) (int64, error) {
	err := e.db.Insert(&rank)
	if err != nil {
		errmsg("CreateRank insert", err)
	}
	return rank.ID, err
}

// UpdateRank - save rank changes
func (e *Edb) UpdateRank(rank Rank) error {
	err := e.db.Update(&rank)
	if err != nil {
		errmsg("UpdateRank update", err)
	}
	return err
}

// DeleteRank - delete rank by id
func (e *Edb) DeleteRank(id int64) error {
	if id == 0 {
		return nil
	}
	_, err := e.db.Model(&Rank{}).
		Where("id = ?", id).
		Delete()
	if err != nil {
		errmsg("DeleteRank delete", err)
	}
	return err
}

func (e *Edb) rankCreateTable() error {
	str := `
		CREATE TABLE IF NOT EXISTS
			ranks (
				id bigserial primary key,
				name text,
				note text,
				created_at TIMESTAMP without time zone,
				updated_at TIMESTAMP without time zone default now(),
				UNIQUE (name)
			)
	`
	_, err := e.db.Exec(str)
	if err != nil {
		errmsg("rankCreateTable exec", err)
	}
	return err
}
<commit_msg>update query<commit_after>package edc

// Rank - struct for rank
type Rank struct {
	ID        int64  `sql:"id" json:"id"`
	Name      string `sql:"name" json:"name"`
	Note      string `sql:"note, null" json:"note"`
	CreatedAt string `sql:"created_at" json:"created_at"`
	UpdatedAt string `sql:"updated_at" json:"updated_at"`
}

// GetRank - get one rank by id
func (e *Edb) GetRank(id int64) (Rank, error) {
	var rank Rank
	if id == 0 {
		return rank, nil
	}
	err := e.db.Model(&rank).
		Where("id = ?", id).
		Select()
	if err != nil {
		errmsg("GetRank select", err)
	}
	return rank, err
}

// GetRankList - get all rank for list
func (e *Edb) GetRankList() ([]Rank, error) {
	var ranks []Rank
	err := e.db.Model(&Rank{}).
		Column("id", "name", "note").
		Order("name ASC").
		Select(&ranks)
	if err != nil {
		errmsg("GetRankList query", err)
	}
	return ranks, err
}

// GetRankSelect - get all rank for select
func (e *Edb) GetRankSelect() ([]SelectItem, error) {
	var ranks []SelectItem
	err := e.db.Model(&Rank{}).
		Column("id", "name").
		Order("name ASC").
		Select(&ranks)
	if err != nil {
		errmsg("GetRankSelect query", err)
	}
	return ranks, err
}

// CreateRank - create new rank
func (e *Edb) CreateRank(rank Rank) (int64, error) {
	err := e.db.Insert(&rank)
	if err != nil {
		errmsg("CreateRank insert", err)
	}
	return rank.ID, err
}

// UpdateRank - save rank changes
func (e *Edb) UpdateRank(rank Rank) error {
	err := e.db.Update(&rank)
	if err != nil {
		errmsg("UpdateRank update", err)
	}
	return err
}

// DeleteRank - delete rank by id
func (e *Edb) DeleteRank(id int64) error {
	if id == 0 {
		return nil
	}
	_, err := e.db.Model(&Rank{}).
		Where("id = ?", id).
		Delete()
	if err != nil {
		errmsg("DeleteRank delete", err)
	}
	return err
}

func (e *Edb) rankCreateTable() error {
	str := `
		CREATE TABLE IF NOT EXISTS
			ranks (
				id bigserial primary key,
				name text,
				note text,
				created_at TIMESTAMP without time zone,
				updated_at TIMESTAMP without time zone default now(),
				UNIQUE (name)
			)
	`
	_, err := e.db.Exec(str)
	if err != nil {
		errmsg("rankCreateTable exec", err)
	}
	return err
}
<|endoftext|>


<commit_before>// Alexandria
//
// Copyright (C) 2015-2016  Colin Benner
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package alexandria

import (
	"strings"
)

// Figure out what type of a document we have
func (s *Scroll) DocumentType() string {
	return s.Type
}

// Return the lines of the last LaTeX comment block without the leading %.  In
// addition, leading and trailing whitespace is removed.
func findMetadataLines(doc string) []string {
	var metadata []string
	for _, line_ := range strings.Split(doc, "\n") {
		line := strings.TrimSpace(line_)
		if line == "" {
			continue
		}

		// If the current line does not begin with a LaTeX comment, we
		// have not yet encountered the last block of comments, so we
		// should discard all data gathered so far.
		if line[0] != '%' {
			metadata = make([]string, 0, 100) // FIXME magic constant
			continue
		}
		// Remove any leading % and whitespaces
		line = strings.TrimLeft(line, "%  \t")
		if line != "" {
			metadata = append(metadata, line)
		}
	}
	return metadata
}

// Parse a comma separated list of tags into a slice, tranfsorming everything
// to lower case.
func parseTags(line string) []string {
	var tags []string
	for _, tag := range strings.Split(line, ",") {
		tmp := strings.TrimSpace(tag)
		if tmp != "" {
			tags = append(tags, tmp)
		}
	}
	return tags
}

// Parse the tags in the given scroll content.  The format of a scroll is
// generally of the following form:
//
//	\LaTeX\ code ...
//
//	% @source Author: Title
//	% @source Lemma 3.2, p. 41
//	% @type proposition, definition
//	% counter-example, analysis, TopOloGY, Weierstraß
//
// In this example, the scroll contains a proposition and is tagged with
// 'counter-example', 'analysis', 'topology' and 'weierstraß'.  It can be found
// in Author: Title as Lemma 3.2 on pase 41.  All the metadata is stored in the
// final block of LaTeX comments.  Also, we simply ignore any empty lines.
func Parse(id, doc string) Scroll {
	// TODO Handle different types of tags: @source, @doctype, @keywords, and normal tags.
	var source []string
	var hidden []string
	var scroll_type string
	var tags []string
	var other_lines []string

	for _, line := range findMetadataLines(doc) {
		switch {
		case strings.HasPrefix(line, "@hidden "):
			hidden = append(hidden, parseTags(strings.TrimPrefix(line, "@hidden "))...)
		case strings.HasPrefix(line, "@source "):
			source = append(source, strings.TrimSpace(strings.TrimPrefix(line, "@source ")))
		case strings.HasPrefix(line, "@type "):
			tmp := strings.TrimSpace(strings.TrimPrefix(line, "@type "))
			for _, type_ := range strings.Split(tmp, ",") {
				// Ignore all but the first type, the other
				// ones are just for searching
				if scroll_type == "" {
					scroll_type = strings.TrimSpace(type_)
					break
				}
			}
		case strings.HasPrefix(line, "@"):
			// Do not strip the @[a-zA-Z0-9\-]** prefix, otherwise
			// there is no way to tell what the line signifies.
			other_lines = append(other_lines, line)
		default:
			tags = append(tags, parseTags(line)...)
		}
	}
	content := stripComments(doc)

	return Scroll{Id: Id(id), Content: content, Type: scroll_type,
		SourceLines: source, Tags: tags, Hidden: hidden,
		OtherLines: other_lines}
}

// Remove all lines that only contain a LaTeX comment.  This removes all the
// medatata from a scroll.
func stripComments(doc string) string {
	var content string
	for _, line_ := range strings.Split(doc, "\n\n") {
		line := strings.TrimSpace(line_)
		if len(line) > 0 && line[0] == '%' {
			continue
		}
		content += line + "\n"
	}
	return strings.TrimSpace(content)
}
<commit_msg>Preserve paragraph breaks<commit_after>// Alexandria
//
// Copyright (C) 2015-2016  Colin Benner
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package alexandria

import (
	"strings"
)

// Figure out what type of a document we have
func (s *Scroll) DocumentType() string {
	return s.Type
}

// Return the lines of the last LaTeX comment block without the leading %.  In
// addition, leading and trailing whitespace is removed.
func findMetadataLines(doc string) []string {
	var metadata []string
	for _, line_ := range strings.Split(doc, "\n") {
		line := strings.TrimSpace(line_)
		if line == "" {
			continue
		}

		// If the current line does not begin with a LaTeX comment, we
		// have not yet encountered the last block of comments, so we
		// should discard all data gathered so far.
		if line[0] != '%' {
			metadata = make([]string, 0, 100) // FIXME magic constant
			continue
		}
		// Remove any leading % and whitespaces
		line = strings.TrimLeft(line, "%  \t")
		if line != "" {
			metadata = append(metadata, line)
		}
	}
	return metadata
}

// Parse a comma separated list of tags into a slice, tranfsorming everything
// to lower case.
func parseTags(line string) []string {
	var tags []string
	for _, tag := range strings.Split(line, ",") {
		tmp := strings.TrimSpace(tag)
		if tmp != "" {
			tags = append(tags, tmp)
		}
	}
	return tags
}

// Parse the tags in the given scroll content.  The format of a scroll is
// generally of the following form:
//
//	\LaTeX\ code ...
//
//	% @source Author: Title
//	% @source Lemma 3.2, p. 41
//	% @type proposition, definition
//	% counter-example, analysis, TopOloGY, Weierstraß
//
// In this example, the scroll contains a proposition and is tagged with
// 'counter-example', 'analysis', 'topology' and 'weierstraß'.  It can be found
// in Author: Title as Lemma 3.2 on pase 41.  All the metadata is stored in the
// final block of LaTeX comments.  Also, we simply ignore any empty lines.
func Parse(id, doc string) Scroll {
	// TODO Handle different types of tags: @source, @doctype, @keywords, and normal tags.
	var source []string
	var hidden []string
	var scroll_type string
	var tags []string
	var other_lines []string

	for _, line := range findMetadataLines(doc) {
		switch {
		case strings.HasPrefix(line, "@hidden "):
			hidden = append(hidden, parseTags(strings.TrimPrefix(line, "@hidden "))...)
		case strings.HasPrefix(line, "@source "):
			source = append(source, strings.TrimSpace(strings.TrimPrefix(line, "@source ")))
		case strings.HasPrefix(line, "@type "):
			tmp := strings.TrimSpace(strings.TrimPrefix(line, "@type "))
			for _, type_ := range strings.Split(tmp, ",") {
				// Ignore all but the first type, the other
				// ones are just for searching
				if scroll_type == "" {
					scroll_type = strings.TrimSpace(type_)
					break
				}
			}
		case strings.HasPrefix(line, "@"):
			// Do not strip the @[a-zA-Z0-9\-]** prefix, otherwise
			// there is no way to tell what the line signifies.
			other_lines = append(other_lines, line)
		default:
			tags = append(tags, parseTags(line)...)
		}
	}
	content := stripComments(doc)

	return Scroll{Id: Id(id), Content: content, Type: scroll_type,
		SourceLines: source, Tags: tags, Hidden: hidden,
		OtherLines: other_lines}
}

// Remove all lines that only contain a LaTeX comment.  This removes all the
// medatata from a scroll.
func stripComments(doc string) string {
	var content string
	for _, line_ := range strings.Split(doc, "\n\n") {
		line := strings.TrimSpace(line_)
		if len(line) > 0 && line[0] == '%' {
			continue
		}
		content += line + "\n\n"
	}
	return strings.TrimSpace(content)
}
<|endoftext|>


<commit_before>package token

// This is a simple package for go that generates randomized base62 encoded tokens based on a single integer.
// It's ideal for shorturl services or for semi-secured randomized api primary keys.
//
// How it Works
//
// `Token` is an alias for `uint64`.
// Its `Token.Encode()` method interface returns a `Base62` encoded string based off of the number.
// Its implementation of the `json.Marshaler` interface encodes and decoded the `Token` to and from the same
// `Base62` encoded string representation.
//
// Basically, the outside world will always address the token as its string equivolent and internally we can
// always be used as an `uint64` for fast, indexed, unique, lookups in various databases.
//
// **IMPORTANT:** Remember to always check for collisions when adding randomized tokens to a database

import (
	"bytes"
	"fmt"
	"math"
	"math/rand"
	"time"
)

const (
	// Base62 is a string respresentation of every possible base62 character
	Base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

	// MaxTokenLength is the largest possible character length of a token
	MaxTokenLength = 11

	// MinTokenLength is the smallest possible character length of a token
	MinTokenLength = 1

	// DefaultTokenLength is the default size of a token
	DefaultTokenLength = MaxTokenLength
)

var (
	base62Len = uint64(len(Base62))
)

// Token is an alias of an int64 that is json marshalled into a base62 encoded token
type Token uint64

// Encode encodes the token into a base62 string
func (t Token) Encode() string {
	number := uint64(t)
	if number == 0 {
		return ""
	}

	var chars []byte
	for number > 0 {
		result := number / base62Len
		remainder := number % base62Len
		chars = append(chars, Base62[remainder])
		number = result
	}

	for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
		chars[i], chars[j] = chars[j], chars[i]
	}

	return string(chars)
}

// UnmarshalJSON implements the `json.Marsheler` interface to decode the token from a base62 string back into an int64
func (t *Token) UnmarshalJSON(data []byte) error {
	str := string(data)
	strLen := len(data)

	// return an error if the json was not a valid string representation of the token
	isString := (str[0] == '"' || str[0] == '\'') && (str[strLen-1] == '"' || str[strLen-1] == '\'')
	if !isString {
		return fmt.Errorf("attempted to parse a non-string token")
	}

	// decode the token
	decoded, err := Decode(str[1 : strLen-1])
	if err != nil {
		return err
	}

	// the token was successfully decoded
	*t = decoded
	return nil
}

// MarshalJSON implements the `json.Marsheler` interface to encode the token into a base62 string
func (t Token) MarshalJSON() ([]byte, error) {
	token := t.Encode()
	if token == "" {
		return []byte{}, nil
	}
	return []byte("\"" + t.Encode() + "\""), nil
}

// New returns a `Base62` encoded `Token` of *up to* `DefaultTokenLength`
// if you pass in a `tokenLength` between `MinTokenLength` and `MaxTokenLength` this will return
// a `Token` of *up to* that length instead if you pass in a `tokenLength` that is out of range it will panic
func New(tokenLength ...int) Token {

	// calculate the max hash int based on the token length
	var max uint64
	if tokenLength != nil {
		isInRange := tokenLength[0] >= MinTokenLength && tokenLength[0] <= MaxTokenLength
		if isInRange {
			max = maxHashInt(tokenLength[0])
		} else {
			panic(fmt.Errorf("tokenLength ∉ [%d,%d]", MinTokenLength, MaxTokenLength))
			return Token(0)
		}
	} else {
		max = maxHashInt(DefaultTokenLength)
	}

	// generate a psuedo random token
	rand.Seed(time.Now().UTC().UnixNano())
	number := uint64(rand.Int63n(int64(max & math.MaxInt64)))

	return Token(number)
}

// Decode returns a token from a 1-12 character base62 encoded string
func Decode(token string) (Token, error) {

	number := uint64(0)
	idx := 0.0
	chars := []byte(Base62)

	charsLength := float64(len(chars))
	tokenLength := float64(len(token))

	if tokenLength > MaxTokenLength {
		return Token(0), fmt.Errorf("%d > MaxTokenLength (%d)", int(tokenLength), MaxTokenLength)
	} else if tokenLength < MinTokenLength {
		return Token(0), fmt.Errorf("%d < MinTokenLength (%d)", int(tokenLength), MinTokenLength)
	}

	for _, c := range []byte(token) {
		power := tokenLength - (idx + 1)
		index := bytes.IndexByte(chars, c)
		if index < 0 {
			return Token(0), fmt.Errorf("%q is not present in %s", c, Base62)
		}
		number += uint64(index) * uint64(math.Pow(charsLength, power))
		idx++
	}

	return Token(number), nil
}

// maxHashInt returns the largest possible int that will yeild a base62 encoded token of the specified length
func maxHashInt(length int) uint64 {
	return uint64(math.Max(0, math.Min(math.MaxUint64, math.Pow(float64(base62Len), float64(length)))))
}
<commit_msg>changed the max token length for now<commit_after>package token

// This is a simple package for go that generates randomized base62 encoded tokens based on a single integer.
// It's ideal for shorturl services or for semi-secured randomized api primary keys.
//
// How it Works
//
// `Token` is an alias for `uint64`.
// Its `Token.Encode()` method interface returns a `Base62` encoded string based off of the number.
// Its implementation of the `json.Marshaler` interface encodes and decoded the `Token` to and from the same
// `Base62` encoded string representation.
//
// Basically, the outside world will always address the token as its string equivolent and internally we can
// always be used as an `uint64` for fast, indexed, unique, lookups in various databases.
//
// **IMPORTANT:** Remember to always check for collisions when adding randomized tokens to a database

import (
	"bytes"
	"fmt"
	"math"
	"math/rand"
	"time"
)

const (
	// Base62 is a string respresentation of every possible base62 character
	Base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

	// MaxTokenLength is the largest possible character length of a token
	MaxTokenLength = 10

	// MinTokenLength is the smallest possible character length of a token
	MinTokenLength = 1

	// DefaultTokenLength is the default size of a token
	DefaultTokenLength = MaxTokenLength
)

var (
	base62Len = uint64(len(Base62))
)

// Token is an alias of an int64 that is json marshalled into a base62 encoded token
type Token uint64

// Encode encodes the token into a base62 string
func (t Token) Encode() string {
	number := uint64(t)
	if number == 0 {
		return ""
	}

	var chars []byte
	for number > 0 {
		result := number / base62Len
		remainder := number % base62Len
		chars = append(chars, Base62[remainder])
		number = result
	}

	for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
		chars[i], chars[j] = chars[j], chars[i]
	}

	return string(chars)
}

// UnmarshalJSON implements the `json.Marsheler` interface to decode the token from a base62 string back into an int64
func (t *Token) UnmarshalJSON(data []byte) error {
	str := string(data)
	strLen := len(data)

	// return an error if the json was not a valid string representation of the token
	isString := (str[0] == '"' || str[0] == '\'') && (str[strLen-1] == '"' || str[strLen-1] == '\'')
	if !isString {
		return fmt.Errorf("attempted to parse a non-string token")
	}

	// decode the token
	decoded, err := Decode(str[1 : strLen-1])
	if err != nil {
		return err
	}

	// the token was successfully decoded
	*t = decoded
	return nil
}

// MarshalJSON implements the `json.Marsheler` interface to encode the token into a base62 string
func (t Token) MarshalJSON() ([]byte, error) {
	token := t.Encode()
	if token == "" {
		return []byte{}, nil
	}
	return []byte("\"" + t.Encode() + "\""), nil
}

// New returns a `Base62` encoded `Token` of *up to* `DefaultTokenLength`
// if you pass in a `tokenLength` between `MinTokenLength` and `MaxTokenLength` this will return
// a `Token` of *up to* that length instead if you pass in a `tokenLength` that is out of range it will panic
func New(tokenLength ...int) Token {

	// calculate the max hash int based on the token length
	var max uint64
	if tokenLength != nil {
		isInRange := tokenLength[0] >= MinTokenLength && tokenLength[0] <= MaxTokenLength
		if isInRange {
			max = maxHashInt(tokenLength[0])
		} else {
			panic(fmt.Errorf("tokenLength ∉ [%d,%d]", MinTokenLength, MaxTokenLength))
			return Token(0)
		}
	} else {
		max = maxHashInt(DefaultTokenLength)
	}

	// generate a psuedo random token
	rand.Seed(time.Now().UTC().UnixNano())
	number := uint64(rand.Int63n(int64(max & math.MaxInt64)))

	return Token(number)
}

// Decode returns a token from a 1-12 character base62 encoded string
func Decode(token string) (Token, error) {

	number := uint64(0)
	idx := 0.0
	chars := []byte(Base62)

	charsLength := float64(len(chars))
	tokenLength := float64(len(token))

	if tokenLength > MaxTokenLength {
		return Token(0), fmt.Errorf("%d > MaxTokenLength (%d)", int(tokenLength), MaxTokenLength)
	} else if tokenLength < MinTokenLength {
		return Token(0), fmt.Errorf("%d < MinTokenLength (%d)", int(tokenLength), MinTokenLength)
	}

	for _, c := range []byte(token) {
		power := tokenLength - (idx + 1)
		index := bytes.IndexByte(chars, c)
		if index < 0 {
			return Token(0), fmt.Errorf("%q is not present in %s", c, Base62)
		}
		number += uint64(index) * uint64(math.Pow(charsLength, power))
		idx++
	}

	return Token(number), nil
}

// maxHashInt returns the largest possible int that will yeild a base62 encoded token of the specified length
func maxHashInt(length int) uint64 {
	return uint64(math.Max(0, math.Min(math.MaxUint64, math.Pow(float64(base62Len), float64(length)))))
}
<|endoftext|>


<commit_before>package arn

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/aerogo/aero"
	"github.com/animenotifier/kitsu"
	"github.com/animenotifier/mal"
	shortid "github.com/ventu-io/go-shortid"
	"github.com/xrash/smetrics"
)

var stripTagsRegex = regexp.MustCompile(`<[^>]*>`)
var sourceRegex = regexp.MustCompile(`\(Source: (.*?)\)`)
var writtenByRegex = regexp.MustCompile(`\[Written by (.*?)\]`)

// GenerateID generates a unique ID for a given table.
func GenerateID(table string) string {
	id, _ := shortid.Generate()

	// Retry until we find an unused ID
	retry := 0

	for {
		_, err := DB.Get(table, id)

		if err != nil && strings.Index(err.Error(), "not found") != -1 {
			return id
		}

		retry++

		if retry > 10 {
			panic(errors.New("Can't generate unique ID"))
		}

		id, _ = shortid.Generate()
	}
}

// GetUserFromContext returns the logged in user for the given context.
func GetUserFromContext(ctx *aero.Context) *User {
	if !ctx.HasSession() {
		return nil
	}

	userID := ctx.Session().GetString("userId")

	if userID == "" {
		return nil
	}

	user, err := GetUser(userID)

	if err != nil {
		return nil
	}

	return user
}

// skipFunc is a function that tells SetObjectProperties whether a property assignment can be skipped
// because it's manually handled inside the class itself. It can also return an error.
type skipFunc func(fullKeyName string, field *reflect.StructField, property *reflect.Value, newValue reflect.Value) (bool, error)

// SetObjectProperties updates the object with the given map[string]interface{}
func SetObjectProperties(rootObj interface{}, updates map[string]interface{}, skip skipFunc) error {
	var t reflect.Type
	var v reflect.Value
	var field reflect.StructField
	var found bool

	for key, value := range updates {
		t = reflect.TypeOf(rootObj).Elem()
		v = reflect.ValueOf(rootObj).Elem()

		if strings.HasPrefix(key, "Custom:") {
			skip(key, nil, nil, reflect.ValueOf(value))
			continue
		}

		// Nested properties
		parts := strings.Split(key, ".")

		for _, part := range parts {
			field, found = t.FieldByName(part)

			if !found {
				return errors.New("Field '" + part + "' does not exist in type " + t.Name())
			}

			t = field.Type
			v = reflect.Indirect(v.FieldByName(field.Name))

			if t.Kind() == reflect.Ptr {
				t = t.Elem()
			}
		}

		newValue := reflect.ValueOf(value)

		// Is somebody attempting to edit fields that aren't editable?
		if field.Tag.Get("editable") != "true" {
			return errors.New("Field " + key + " is not editable")
		}

		// Is this manually handled by the class so we can skip it?
		// Also make sure to pass full "key" value here instead of "fieldName".
		if skip != nil {
			canSkip, err := skip(key, &field, &v, newValue)

			if err != nil {
				return err
			}

			if canSkip {
				continue
			}
		}

		// Implement special data type cases here
		if v.Kind() == reflect.Int {
			x := int64(newValue.Float())

			if !v.OverflowInt(x) {
				v.SetInt(x)
			}
		} else {
			v.Set(newValue)
		}
	}

	return nil
}

// GetGenreIDByName ...
func GetGenreIDByName(genre string) string {
	genre = strings.Replace(genre, "-", "", -1)
	genre = strings.Replace(genre, " ", "", -1)
	genre = strings.ToLower(genre)
	return genre
}

// FixAnimeDescription ...
func FixAnimeDescription(description string) string {
	description = stripTagsRegex.ReplaceAllString(description, "")
	description = sourceRegex.ReplaceAllString(description, "")
	description = writtenByRegex.ReplaceAllString(description, "")
	return strings.TrimSpace(description)
}

// FixGender ...
func FixGender(gender string) string {
	if gender != "male" && gender != "female" {
		return ""
	}

	return gender
}

// AnimeRatingStars displays the rating in Unicode stars.
func AnimeRatingStars(rating float64) string {
	stars := int(rating/20 + 0.5)
	return strings.Repeat("★", stars) + strings.Repeat("☆", 5-stars)
}

// EpisodesToString shows a question mark if the episode count is zero.
func EpisodesToString(episodes int) string {
	if episodes == 0 {
		return "?"
	}

	return ToString(episodes)
}

// EpisodeCountMax is used for the max value of number input on episodes.
func EpisodeCountMax(episodes int) string {
	if episodes == 0 {
		return ""
	}

	return strconv.Itoa(episodes)
}

// DateTimeUTC returns the current UTC time in RFC3339 format.
func DateTimeUTC() string {
	return time.Now().UTC().Format(time.RFC3339)
}

// StringSimilarity returns 1.0 if the strings are equal and goes closer to 0 when they are different.
func StringSimilarity(a string, b string) float64 {
	return smetrics.JaroWinkler(a, b, 0.7, 4)
}

// OverallRatingName returns Overall in general, but Hype when episodes watched is zero.
func OverallRatingName(episodes int) string {
	if episodes == 0 {
		return "Hype"
	}

	return "Overall"
}

// MyAnimeListStatusToARNStatus ...
func MyAnimeListStatusToARNStatus(status string) string {
	switch status {
	case mal.AnimeListStatusCompleted:
		return AnimeListStatusCompleted
	case mal.AnimeListStatusWatching:
		return AnimeListStatusWatching
	case mal.AnimeListStatusPlanned:
		return AnimeListStatusPlanned
	case mal.AnimeListStatusHold:
		return AnimeListStatusHold
	case mal.AnimeListStatusDropped:
		return AnimeListStatusDropped
	default:
		return ""
	}
}

// KitsuStatusToARNStatus ...
func KitsuStatusToARNStatus(status string) string {
	switch status {
	case kitsu.AnimeListStatusCompleted:
		return AnimeListStatusCompleted
	case kitsu.AnimeListStatusWatching:
		return AnimeListStatusWatching
	case kitsu.AnimeListStatusPlanned:
		return AnimeListStatusPlanned
	case kitsu.AnimeListStatusHold:
		return AnimeListStatusHold
	case kitsu.AnimeListStatusDropped:
		return AnimeListStatusDropped
	default:
		return ""
	}
}

// ListItemStatusName ...
func ListItemStatusName(status string) string {
	switch status {
	case AnimeListStatusWatching:
		return "Watching"
	case AnimeListStatusCompleted:
		return "Completed"
	case AnimeListStatusPlanned:
		return "Planned"
	case AnimeListStatusHold:
		return "On hold"
	case AnimeListStatusDropped:
		return "Dropped"
	default:
		return ""
	}
}

// PanicOnError will panic if the error is not nil.
func PanicOnError(err error) {
	if err != nil {
		panic(err)
	}
}

// PrettyPrint prints the object as indented JSON data on the console.
func PrettyPrint(obj interface{}) {
	pretty, _ := json.MarshalIndent(obj, "", "\t")
	fmt.Println(string(pretty))
}
<commit_msg>Added array index support<commit_after>package arn

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/aerogo/aero"
	"github.com/animenotifier/kitsu"
	"github.com/animenotifier/mal"
	shortid "github.com/ventu-io/go-shortid"
	"github.com/xrash/smetrics"
)

var stripTagsRegex = regexp.MustCompile(`<[^>]*>`)
var sourceRegex = regexp.MustCompile(`\(Source: (.*?)\)`)
var writtenByRegex = regexp.MustCompile(`\[Written by (.*?)\]`)

// GenerateID generates a unique ID for a given table.
func GenerateID(table string) string {
	id, _ := shortid.Generate()

	// Retry until we find an unused ID
	retry := 0

	for {
		_, err := DB.Get(table, id)

		if err != nil && strings.Index(err.Error(), "not found") != -1 {
			return id
		}

		retry++

		if retry > 10 {
			panic(errors.New("Can't generate unique ID"))
		}

		id, _ = shortid.Generate()
	}
}

// GetUserFromContext returns the logged in user for the given context.
func GetUserFromContext(ctx *aero.Context) *User {
	if !ctx.HasSession() {
		return nil
	}

	userID := ctx.Session().GetString("userId")

	if userID == "" {
		return nil
	}

	user, err := GetUser(userID)

	if err != nil {
		return nil
	}

	return user
}

// skipFunc is a function that tells SetObjectProperties whether a property assignment can be skipped
// because it's manually handled inside the class itself. It can also return an error.
type skipFunc func(fullKeyName string, field *reflect.StructField, property *reflect.Value, newValue reflect.Value) (bool, error)

// SetObjectProperties updates the object with the given map[string]interface{}
func SetObjectProperties(rootObj interface{}, updates map[string]interface{}, skip skipFunc) error {
	var t reflect.Type
	var v reflect.Value
	var field reflect.StructField
	var found bool

	for key, value := range updates {
		t = reflect.TypeOf(rootObj).Elem()
		v = reflect.ValueOf(rootObj).Elem()

		if strings.HasPrefix(key, "Custom:") {
			skip(key, nil, nil, reflect.ValueOf(value))
			continue
		}

		// Nested properties
		parts := strings.Split(key, ".")

		for _, part := range parts {
			if strings.HasSuffix(part, "]") {
				// Array reference
				arrayStart := strings.Index(part, "[")
				arrayIndexString := part[arrayStart+1 : len(part)-1]
				arrayIndex, err := strconv.Atoi(arrayIndexString)
				part = part[:arrayStart]

				if err != nil {
					return err
				}

				// Get the slice first
				field, found = t.FieldByName(part)

				if !found {
					return errors.New("Field '" + part + "' does not exist in type " + t.Name())
				}

				v = reflect.Indirect(v.FieldByName(field.Name))

				// Now get the object referenced at the given index
				v = reflect.Indirect(v.Index(arrayIndex))
				t = v.Type()
			} else {
				// Non-array reference
				field, found = t.FieldByName(part)

				if !found {
					return errors.New("Field '" + part + "' does not exist in type " + t.Name())
				}

				t = field.Type
				v = reflect.Indirect(v.FieldByName(field.Name))
			}

			if t.Kind() == reflect.Ptr {
				t = t.Elem()
			}
		}

		newValue := reflect.ValueOf(value)

		// Is somebody attempting to edit fields that aren't editable?
		if field.Tag.Get("editable") != "true" {
			return errors.New("Field " + key + " is not editable")
		}

		// Is this manually handled by the class so we can skip it?
		// Also make sure to pass full "key" value here instead of "fieldName".
		if skip != nil {
			canSkip, err := skip(key, &field, &v, newValue)

			if err != nil {
				return err
			}

			if canSkip {
				continue
			}
		}

		// Implement special data type cases here
		if v.Kind() == reflect.Int {
			x := int64(newValue.Float())

			if !v.OverflowInt(x) {
				v.SetInt(x)
			}
		} else {
			v.Set(newValue)
		}
	}

	return nil
}

// GetGenreIDByName ...
func GetGenreIDByName(genre string) string {
	genre = strings.Replace(genre, "-", "", -1)
	genre = strings.Replace(genre, " ", "", -1)
	genre = strings.ToLower(genre)
	return genre
}

// FixAnimeDescription ...
func FixAnimeDescription(description string) string {
	description = stripTagsRegex.ReplaceAllString(description, "")
	description = sourceRegex.ReplaceAllString(description, "")
	description = writtenByRegex.ReplaceAllString(description, "")
	return strings.TrimSpace(description)
}

// FixGender ...
func FixGender(gender string) string {
	if gender != "male" && gender != "female" {
		return ""
	}

	return gender
}

// AnimeRatingStars displays the rating in Unicode stars.
func AnimeRatingStars(rating float64) string {
	stars := int(rating/20 + 0.5)
	return strings.Repeat("★", stars) + strings.Repeat("☆", 5-stars)
}

// EpisodesToString shows a question mark if the episode count is zero.
func EpisodesToString(episodes int) string {
	if episodes == 0 {
		return "?"
	}

	return ToString(episodes)
}

// EpisodeCountMax is used for the max value of number input on episodes.
func EpisodeCountMax(episodes int) string {
	if episodes == 0 {
		return ""
	}

	return strconv.Itoa(episodes)
}

// DateTimeUTC returns the current UTC time in RFC3339 format.
func DateTimeUTC() string {
	return time.Now().UTC().Format(time.RFC3339)
}

// StringSimilarity returns 1.0 if the strings are equal and goes closer to 0 when they are different.
func StringSimilarity(a string, b string) float64 {
	return smetrics.JaroWinkler(a, b, 0.7, 4)
}

// OverallRatingName returns Overall in general, but Hype when episodes watched is zero.
func OverallRatingName(episodes int) string {
	if episodes == 0 {
		return "Hype"
	}

	return "Overall"
}

// MyAnimeListStatusToARNStatus ...
func MyAnimeListStatusToARNStatus(status string) string {
	switch status {
	case mal.AnimeListStatusCompleted:
		return AnimeListStatusCompleted
	case mal.AnimeListStatusWatching:
		return AnimeListStatusWatching
	case mal.AnimeListStatusPlanned:
		return AnimeListStatusPlanned
	case mal.AnimeListStatusHold:
		return AnimeListStatusHold
	case mal.AnimeListStatusDropped:
		return AnimeListStatusDropped
	default:
		return ""
	}
}

// KitsuStatusToARNStatus ...
func KitsuStatusToARNStatus(status string) string {
	switch status {
	case kitsu.AnimeListStatusCompleted:
		return AnimeListStatusCompleted
	case kitsu.AnimeListStatusWatching:
		return AnimeListStatusWatching
	case kitsu.AnimeListStatusPlanned:
		return AnimeListStatusPlanned
	case kitsu.AnimeListStatusHold:
		return AnimeListStatusHold
	case kitsu.AnimeListStatusDropped:
		return AnimeListStatusDropped
	default:
		return ""
	}
}

// ListItemStatusName ...
func ListItemStatusName(status string) string {
	switch status {
	case AnimeListStatusWatching:
		return "Watching"
	case AnimeListStatusCompleted:
		return "Completed"
	case AnimeListStatusPlanned:
		return "Planned"
	case AnimeListStatusHold:
		return "On hold"
	case AnimeListStatusDropped:
		return "Dropped"
	default:
		return ""
	}
}

// PanicOnError will panic if the error is not nil.
func PanicOnError(err error) {
	if err != nil {
		panic(err)
	}
}

// PrettyPrint prints the object as indented JSON data on the console.
func PrettyPrint(obj interface{}) {
	pretty, _ := json.MarshalIndent(obj, "", "\t")
	fmt.Println(string(pretty))
}
<|endoftext|>