mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
206 lines
4.4 KiB
Go
206 lines
4.4 KiB
Go
// based on code from golang src/time/time.go
|
|
|
|
package mpd
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Duration time.Duration
|
|
|
|
var (
|
|
rStart = "^P" // Must start with a 'P'
|
|
rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years
|
|
rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T'
|
|
rHours = "(\\d+H)?" // Hours
|
|
rMinutes = "(\\d+M)?" // Minutes
|
|
rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal)
|
|
rEnd = ")?$" // end of regex must close "T" capture group
|
|
)
|
|
|
|
var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd)
|
|
|
|
func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
|
|
return xml.Attr{Name: name, Value: d.String()}, nil
|
|
}
|
|
|
|
func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
|
|
dur, err := ParseDuration(attr.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*d = Duration(dur)
|
|
return nil
|
|
}
|
|
|
|
// String renders a Duration in XML Duration Data Type format
|
|
func (d *Duration) String() string {
|
|
// Largest time is 2540400h10m10.000000000s
|
|
var buf [32]byte
|
|
w := len(buf)
|
|
|
|
u := uint64(*d)
|
|
neg := *d < 0
|
|
if neg {
|
|
u = -u
|
|
}
|
|
|
|
if u < uint64(time.Second) {
|
|
// Special case: if duration is smaller than a second,
|
|
// use smaller units, like 1.2ms
|
|
var prec int
|
|
w--
|
|
buf[w] = 'S'
|
|
w--
|
|
if u == 0 {
|
|
return "PT0S"
|
|
}
|
|
/*
|
|
switch {
|
|
case u < uint64(Millisecond):
|
|
// print microseconds
|
|
prec = 3
|
|
// U+00B5 'µ' micro sign == 0xC2 0xB5
|
|
w-- // Need room for two bytes.
|
|
copy(buf[w:], "µ")
|
|
default:
|
|
// print milliseconds
|
|
prec = 6
|
|
buf[w] = 'm'
|
|
}
|
|
*/
|
|
w, u = fmtFrac(buf[:w], u, prec)
|
|
w = fmtInt(buf[:w], u)
|
|
} else {
|
|
w--
|
|
buf[w] = 'S'
|
|
|
|
w, u = fmtFrac(buf[:w], u, 9)
|
|
|
|
// u is now integer seconds
|
|
w = fmtInt(buf[:w], u%60)
|
|
u /= 60
|
|
|
|
// u is now integer minutes
|
|
if u > 0 {
|
|
w--
|
|
buf[w] = 'M'
|
|
w = fmtInt(buf[:w], u%60)
|
|
u /= 60
|
|
|
|
// u is now integer hours
|
|
// Stop at hours because days can be different lengths.
|
|
if u > 0 {
|
|
w--
|
|
buf[w] = 'H'
|
|
w = fmtInt(buf[:w], u)
|
|
}
|
|
}
|
|
}
|
|
|
|
if neg {
|
|
w--
|
|
buf[w] = '-'
|
|
}
|
|
|
|
return "PT" + string(buf[w:])
|
|
}
|
|
|
|
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
|
|
// tail of buf, omitting trailing zeros. it omits the decimal
|
|
// point too when the fraction is 0. It returns the index where the
|
|
// output bytes begin and the value v/10**prec.
|
|
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
|
|
// Omit trailing zeros up to and including decimal point.
|
|
w := len(buf)
|
|
print := false
|
|
for i := 0; i < prec; i++ {
|
|
digit := v % 10
|
|
print = print || digit != 0
|
|
if print {
|
|
w--
|
|
buf[w] = byte(digit) + '0'
|
|
}
|
|
v /= 10
|
|
}
|
|
if print {
|
|
w--
|
|
buf[w] = '.'
|
|
}
|
|
return w, v
|
|
}
|
|
|
|
// fmtInt formats v into the tail of buf.
|
|
// It returns the index where the output begins.
|
|
func fmtInt(buf []byte, v uint64) int {
|
|
w := len(buf)
|
|
if v == 0 {
|
|
w--
|
|
buf[w] = '0'
|
|
} else {
|
|
for v > 0 {
|
|
w--
|
|
buf[w] = byte(v%10) + '0'
|
|
v /= 10
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
func ParseDuration(str string) (time.Duration, error) {
|
|
if len(str) < 3 {
|
|
return 0, errors.New("At least one number and designator are required")
|
|
}
|
|
|
|
if strings.Contains(str, "-") {
|
|
return 0, errors.New("Duration cannot be negative")
|
|
}
|
|
|
|
// Check that only the parts we expect exist and that everything's in the correct order
|
|
if !xmlDurationRegex.Match([]byte(str)) {
|
|
return 0, errors.New("Duration must be in the format: P[nD][T[nH][nM][nS]]")
|
|
}
|
|
|
|
var parts = xmlDurationRegex.FindStringSubmatch(str)
|
|
var total time.Duration
|
|
|
|
if parts[1] != "" {
|
|
days, err := strconv.Atoi(strings.TrimRight(parts[1], "D"))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("Error parsing Days: %s", err)
|
|
}
|
|
total += time.Duration(days) * time.Hour * 24
|
|
}
|
|
|
|
if parts[2] != "" {
|
|
hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H"))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("Error parsing Hours: %s", err)
|
|
}
|
|
total += time.Duration(hours) * time.Hour
|
|
}
|
|
|
|
if parts[3] != "" {
|
|
mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M"))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("Error parsing Minutes: %s", err)
|
|
}
|
|
total += time.Duration(mins) * time.Minute
|
|
}
|
|
|
|
if parts[4] != "" {
|
|
secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("Error parsing Seconds: %s", err)
|
|
}
|
|
total += time.Duration(secs * float64(time.Second))
|
|
}
|
|
|
|
return total, nil
|
|
}
|