tracker_metadata.go 28.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package varroa

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strconv"
	"strings"
antipathique's avatar
antipathique committed
17
	"text/template"
18
19
	"time"

20
	humanize "github.com/dustin/go-humanize"
21
	"github.com/mewkiz/flac"
22
23
	"github.com/mgutz/ansi"
	"github.com/pkg/errors"
24
	"github.com/russross/blackfriday"
25
	"gitlab.com/catastrophic/assistance/fs"
26
	"gitlab.com/catastrophic/assistance/logthis"
user's avatar
user committed
27
	"gitlab.com/catastrophic/assistance/music"
28
	"gitlab.com/passelecasque/obstruction/tracker"
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
)

const (

	// TODO: add track durations + total duration
	// TODO: lineage: Discogs: XXX; Qobuz: XXX; etc...
	// TODO: add last.fm/discogs/etc info too?
	mdTemplate = `# %s - %s (%d)

![cover](%s)

**Tags:** %s

## Release

**Type:** %s

**Label:** %s

**Catalog Number:** %s

**Source:** %s
%s
## Audio

**Format:** %s

**Quality:** %s

## Tracklist

%s

## Lineage

%s

## Origin

Automatically generated on %s.

Torrent is %s on %s.

Direct link: %s

`
	remasterTemplate = `
**Remaster Label**: %s

**Remaster Catalog Number:** %s

**Remaster Year:** %d

**Edition name:** %s
`
antipathique's avatar
antipathique committed
84
85
	txtDescription = `
┌──────────
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
│ %s
└─┬────────
  │  Release Type: %s
  │  Year: %s
  │  Tags: %s
  │  Record Label: %s
  │  Catalog Number: %s
  │  Edition Name: %s
  │  Tracks: %s	
  ├────────
  │  Source: %s
  │  Format: %s
  │  Quality: %s
  ├────────	
  │  Tracker: %s
antipathique's avatar
antipathique committed
101
102
  │  ID: %s
  │  GroupID: %s
103
104
  │  Release Link: %s
  │  Cover: %s	
antipathique's avatar
antipathique committed
105
  │  Size: %s	
106
  └────────`
107
	vaReleasePrexif = "VA|"
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
)

type TrackerMetadataTrack struct {
	Disc     string
	Number   string
	Title    string
	Duration string
	Size     string
}

func (rit *TrackerMetadataTrack) String() string {
	return fmt.Sprintf("+ %s [%s]", rit.Title, rit.Size)
}

type TrackerMetadataArtist struct {
	ID   int
	Name string
	Role string
	JSON []byte `json:"-"`
}

type TrackerMetadataLineage struct {
	Source            string
	LinkOrDescription string
}

type TrackerMetadata struct {
	// JSONs
	ReleaseJSON []byte `json:"-"`
	OriginJSON  []byte `json:"-"`
	// tracker related metadata
139
140
141
142
143
144
145
146
147
148
149
150
151
	ID           int
	GroupID      int
	Tracker      string
	TrackerURL   string
	ReleaseURL   string
	TimeSnatched int64
	LastUpdated  int64
	IsAlive      bool
	Size         uint64
	Uploader     string
	FolderName   string
	CoverURL     string

152
153
154
155
156
157
158
159
160
161
	// release related metadata
	Artists       []TrackerMetadataArtist
	Title         string
	Tags          []string
	ReleaseType   string
	RecordLabel   string
	CatalogNumber string
	OriginalYear  int
	EditionName   string
	EditionYear   int
162
163
164
165
166
167
168
169
170
171
172
173
174
	Source        string
	SourceFull    string
	Format        string
	Quality       string
	LogScore      int
	HasLog        bool
	HasCue        bool
	IsScene       bool
	// for library organization
	MainArtist      string
	MainArtistAlias string
	Category        string
	// contents
175
176
177
178
	Tracks      []TrackerMetadataTrack
	TotalTime   string
	Lineage     []TrackerMetadataLineage
	Description string
179
180
181
	// current tracker state
	CurrentSeeders int  `json:"-"`
	Reported       bool `json:"-"`
182
	Trumpable      bool `json:"-"`
183
	ApprovedLossy  bool `json:"-"`
184
185
186
}

func (tm *TrackerMetadata) LoadFromJSON(tracker string, originJSON, releaseJSON string) error {
187
	if !fs.FileExists(originJSON) || !fs.FileExists(releaseJSON) {
188
189
190
191
192
193
		return errors.New("error loading file " + releaseJSON + " or " + releaseJSON + ", which could not be found")
	}

	// load Origin JSON
	var err error
	origin := TrackerOriginJSON{Path: originJSON}
user's avatar
user committed
194
	if err = origin.Load(); err != nil {
195
196
197
198
199
200
201
202
203
		return err
	}
	// getting the information
	tm.TimeSnatched = origin.Origins[tracker].TimeSnatched
	tm.LastUpdated = origin.Origins[tracker].LastUpdatedMetadata
	tm.IsAlive = origin.Origins[tracker].IsAlive
	tm.Tracker = tracker
	tm.TrackerURL = origin.Origins[tracker].Tracker

204
	// load Release JSON
205
206
207
208
	tm.ReleaseJSON, err = ioutil.ReadFile(releaseJSON)
	if err != nil {
		return errors.Wrap(err, "Error loading JSON file "+releaseJSON)
	}
209
	return tm.loadReleaseJSONFromBytes(filepath.Dir(releaseJSON), true)
210
211
}

212
func (tm *TrackerMetadata) saveOriginJSON(destination string) error {
user's avatar
user committed
213
	origin := &TrackerOriginJSON{Path: filepath.Join(destination, OriginJSONFile)}
214
215

	foundOrigin := false
216
	if fs.FileExists(origin.Path) {
user's avatar
user committed
217
		if err := origin.Load(); err != nil {
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
			return err
		}
		for i, o := range origin.Origins {
			if i == tm.Tracker && o.ID == tm.ID {
				origin.Origins[i].LastUpdatedMetadata = tm.LastUpdated
				origin.Origins[i].IsAlive = tm.IsAlive
				// may have been edited
				origin.Origins[i].GroupID = tm.GroupID
				foundOrigin = true
			}
		}
	}
	if !foundOrigin {
		if origin.Origins == nil {
			origin.Origins = make(map[string]*OriginJSON)
		}
		// creating origin
		origin.Origins[tm.Tracker] = &OriginJSON{Tracker: tm.TrackerURL, ID: tm.ID, GroupID: tm.GroupID, TimeSnatched: tm.TimeSnatched, LastUpdatedMetadata: tm.LastUpdated, IsAlive: tm.IsAlive}
	}
	return origin.write()
}

240
func (tm *TrackerMetadata) Load(t *tracker.Gazelle, info *tracker.GazelleTorrent) error {
241
	// recreate Origin JSON data from tracker
242
243
244
	tm.Tracker = t.Name
	tm.TrackerURL = t.DomainURL
	tm.TimeSnatched = time.Now().Unix()
245
246
	tm.LastUpdated = time.Now().Unix()
	tm.IsAlive = true
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
	return tm.loadFromGazelle(info)
}

// LoadFromID directly from tracker
func (tm *TrackerMetadata) LoadFromID(t *tracker.Gazelle, id string) error {
	// get data from tracker & load
	torrentID, convErr := strconv.Atoi(id)
	if convErr != nil {
		return errors.Wrap(convErr, errorCouldNotGetTorrentInfo)
	}
	gzTorrent, err := t.GetTorrent(torrentID)
	if err != nil {
		return errors.Wrap(err, errorCouldNotGetTorrentInfo)
	}
	return tm.Load(t, gzTorrent)
262
263
}

264
func (tm *TrackerMetadata) loadReleaseJSONFromBytes(parentFolder string, responseOnly bool) error {
265
	var gt tracker.GazelleTorrent
266
267
268
	var unmarshalErr error
	if responseOnly {
		unmarshalErr = json.Unmarshal(tm.ReleaseJSON, &gt)
269
270
271
272
273
274
	} else {
		var gtr tracker.GazelleTorrentResponse
		unmarshalErr = json.Unmarshal(tm.ReleaseJSON, &gtr)
		if unmarshalErr == nil {
			gt = gtr.Response
		}
275
276
	}
	if unmarshalErr != nil {
277
		logthis.Error(errors.Wrap(unmarshalErr, "Error parsing torrent info JSON"), logthis.NORMAL)
278
279
		return nil
	}
280
281
282
283
	if err := tm.loadFromGazelle(&gt); err != nil {
		logthis.Error(errors.Wrap(unmarshalErr, "Error parsing torrent info"), logthis.NORMAL)
		return err
	}
284

285
286
287
288
289
290
291
292
293
294
295
	// finally, load user JSON for overwriting user-defined values, if loading from files
	if responseOnly {
		if err := tm.LoadUserJSON(parentFolder); err != nil {
			return err
		}
	}
	// try to find if the configuration has overriding artist aliases/categories
	return tm.checkAliasAndCategory(parentFolder)
}

func (tm *TrackerMetadata) loadFromGazelle(info *tracker.GazelleTorrent) error {
296
	// tracker related metadata
297
298
299
300
	tm.ID = info.Torrent.ID
	tm.ReleaseURL = tm.TrackerURL + fmt.Sprintf("/torrents.php?torrentid=%d", info.Torrent.ID)
	tm.GroupID = info.Group.ID
	tm.Size = uint64(info.Torrent.Size)
301
	// keeping a copy of uploader before anonymizing
302
303
304
305
306
	tm.Uploader = info.Torrent.Username
	tm.FolderName = html.UnescapeString(info.Torrent.FilePath)
	tm.CoverURL = info.Group.WikiImage
	tm.CurrentSeeders = info.Torrent.Seeders
	tm.Reported = info.Torrent.Reported
307
	tm.Trumpable = info.Torrent.Trumpable
308
	tm.ApprovedLossy = info.Torrent.LossyMasterApproved || info.Torrent.LossyWebApproved
309
310
311
312

	// release related metadata
	// for now, using artists, composers, "with" categories
	// also available: .Conductor, .Dj, .Producer, .RemixedBy
313
	for _, el := range info.Group.MusicInfo.Artists {
314
315
		tm.Artists = append(tm.Artists, TrackerMetadataArtist{ID: el.ID, Name: html.UnescapeString(el.Name), Role: "Main"})
	}
316
	for _, el := range info.Group.MusicInfo.With {
317
318
		tm.Artists = append(tm.Artists, TrackerMetadataArtist{ID: el.ID, Name: html.UnescapeString(el.Name), Role: "Featuring"})
	}
319
	for _, el := range info.Group.MusicInfo.Composers {
320
321
		tm.Artists = append(tm.Artists, TrackerMetadataArtist{ID: el.ID, Name: html.UnescapeString(el.Name), Role: "Composer"})
	}
322
323
324
325
326
327
	tm.Title = html.UnescapeString(info.Group.Name)
	tm.Tags = info.Group.Tags
	tm.ReleaseType = tracker.GazelleReleaseType(info.Group.ReleaseType)
	tm.RecordLabel = html.UnescapeString(info.Group.RecordLabel)
	if info.Torrent.Remastered && info.Torrent.RemasterRecordLabel != "" {
		tm.RecordLabel = html.UnescapeString(info.Torrent.RemasterRecordLabel)
328
	}
329
330
331
	tm.CatalogNumber = info.Group.CatalogueNumber
	if info.Torrent.Remastered && info.Torrent.RemasterCatalogueNumber != "" {
		tm.CatalogNumber = html.UnescapeString(info.Torrent.RemasterCatalogueNumber)
332
	}
333
334
335
336
337
338
339
340
341
342
	tm.OriginalYear = info.Group.Year
	tm.EditionName = html.UnescapeString(info.Torrent.RemasterTitle)
	tm.EditionYear = info.Torrent.RemasterYear
	tm.Source = html.UnescapeString(info.Torrent.Media)
	tm.Format = info.Torrent.Format
	tm.Quality = info.Torrent.Encoding
	tm.LogScore = info.Torrent.LogScore
	tm.HasLog = info.Torrent.HasLog
	tm.HasCue = info.Torrent.HasCue
	tm.IsScene = info.Torrent.Scene
343
344

	tm.SourceFull = tm.Source
345
	if tm.SourceFull == tracker.SourceCD && tm.Quality == tracker.QualityLossless {
346
		if tm.HasLog && tm.HasCue && (tm.LogScore == 100 || info.Torrent.Grade == "Silver") {
347
348
			tm.SourceFull += "+"
		}
349
		if info.Torrent.Grade == "Gold" {
350
351
352
			tm.SourceFull += "+"
		}
	}
353
354
355
	if tm.ApprovedLossy {
		tm.SourceFull += "*"
	}
356

357
	// parsing info that needs to be worked on before use
358
359

	// default organization info
360
361
362
363
364
365
366
367
368
	var artists []string
	for _, a := range tm.Artists {
		// not taking feat. artists
		if a.Role == "Main" || a.Role == "Composer" {
			artists = append(artists, a.Name)
		}
	}
	tm.MainArtist = strings.Join(artists, ", ")
	if len(artists) >= 3 {
369
		tm.MainArtist = tracker.VariousArtists
370
371
372
373
374
375
376
377
378
379
380
	}

	// default: artist alias = main artist
	tm.MainArtistAlias = tm.MainArtist
	// default: category == first tag
	if len(tm.Tags) != 0 {
		tm.Category = tm.Tags[0]
	} else {
		tm.Category = "UNKNOWN"
	}

381
	// parsing track list
382
	r := regexp.MustCompile(tracker.TrackPattern)
383
	files := strings.Split(info.Torrent.FileList, "|||")
384
385
386
387
388
	for _, f := range files {
		track := TrackerMetadataTrack{}
		hits := r.FindAllStringSubmatch(f, -1)
		if len(hits) != 0 {
			// TODO instead of path, actually find the title
389
			// only detect actual music files
390
391
392
393
394
			track.Title = html.UnescapeString(hits[0][1])
			size, _ := strconv.ParseUint(hits[0][2], 10, 64)
			track.Size = humanize.IBytes(size)
			tm.Tracks = append(tm.Tracks, track)
			// TODO Duration  + Disc + number
395
		}
user's avatar
user committed
396
397
398
	}
	if len(tm.Tracks) == 0 {
		logthis.Info("Could not parse filelist, no music tracks found.", logthis.VERBOSEST)
399
400
401
402
	}
	// TODO tm.TotalTime

	// TODO find other info, parse for discogs/musicbrainz/itunes links in both descriptions
403
404
	if info.Torrent.Description != "" {
		tm.Lineage = append(tm.Lineage, TrackerMetadataLineage{Source: "TorrentDescription", LinkOrDescription: html.UnescapeString(info.Torrent.Description)})
405
	}
406
	// TODO add info.Response.Torrent.Lineage if not empty?
407
408

	// TODO de-wikify
409
	tm.Description = html.UnescapeString(info.Group.WikiBody)
410
411

	// json for metadata, anonymized
412
413
	info.Torrent.Username = ""
	info.Torrent.UserID = 0
414
	// keeping a copy of the full JSON
415
	metadataJSON, err := json.MarshalIndent(info, "", "    ")
416
417
418
419
	if err != nil {
		metadataJSON = tm.ReleaseJSON // falling back to complete json
	}
	tm.ReleaseJSON = metadataJSON
420
	return nil
421
422
423
}

func (tm *TrackerMetadata) checkAliasAndCategory(parentFolder string) error {
424
425
426
427
428
429
430
431
	conf, configErr := NewConfig(DefaultConfigurationFile)
	if configErr != nil {
		return configErr
	}
	if conf.LibraryConfigured {
		var changed bool
		// try to find main artist alias
		for alias, aliasArtists := range conf.Library.Aliases {
432
			if artistInSlice(tm.MainArtist, tm.Title, aliasArtists) {
433
434
435
436
437
438
439
				tm.MainArtistAlias = alias
				changed = true
				break
			}
		}
		// try to find category for main artist alias
		for category, categoryArtists := range conf.Library.Categories {
440
			if artistInSlice(tm.MainArtistAlias, tm.Title, categoryArtists) {
441
442
443
444
445
446
				tm.Category = category
				changed = true
				break
			}
		}
		if changed {
447
			logthis.Info("Updating user metadata with information from the configuration.", logthis.VERBOSEST)
448
449
			return tm.UpdateUserJSON(parentFolder, tm.MainArtist, tm.MainArtistAlias, tm.Category)
		}
antipathique's avatar
antipathique committed
450
451
	}
	return nil
452
453
}

454
455
456
// artistInSlice checks if an artist is in a []string (taking VA releases into account), returns bool.
func artistInSlice(artist, title string, list []string) bool {
	for _, b := range list {
457
		if artist == b || artist == tracker.VariousArtists && title == strings.TrimSpace(strings.Replace(b, vaReleasePrexif, "", -1)) {
458
459
460
461
462
463
			return true
		}
	}
	return false
}

464
// SaveFromTracker all of the relevant metadata.
465
func (tm *TrackerMetadata) SaveFromTracker(parentFolder string, t *tracker.Gazelle) error {
466
467
468
469
470
	c, err := NewConfig(DefaultConfigurationFile)
	if err != nil {
		return err
	}

user's avatar
user committed
471
	destination := filepath.Join(parentFolder, MetadataDir)
472
	// create metadata dir if necessary
473
	if err := os.MkdirAll(destination, 0775); err != nil {
474
475
476
		return errors.Wrap(err, errorCreatingMetadataDir)
	}
	// creating or updating origin.json
477
	if err := tm.saveOriginJSON(destination); err != nil {
478
479
480
		return errors.Wrap(err, errorWithOriginJSON)
	}

481
	// NOTE: errors are not returned (for now) in case the next JSONs can be retrieved
482

483
	logthis.Info(fmt.Sprintf(infoAllMetadataSaving, filepath.Base(parentFolder)), logthis.VERBOSE)
484
	// write tracker metadata to target folder
485
	if err := ioutil.WriteFile(filepath.Join(destination, releaseMetadataFile(tm.Tracker)), tm.ReleaseJSON, 0666); err != nil {
486
		logthis.Error(errors.Wrap(err, errorWritingJSONMetadata), logthis.NORMAL)
487
	} else {
488
		logthis.Info(infoMetadataSaved, logthis.VERBOSE)
489
490
491
	}

	// get torrent group info
492
	gzTorrentGroup, err := t.GetTorrentGroup(tm.GroupID)
493
	if err != nil {
494
		logthis.Info(fmt.Sprintf(errorRetrievingTorrentGroupInfo, tm.GroupID), logthis.NORMAL)
495
	} else {
496
497
498
		// anonymizing data
		gzTorrentGroup.Anonymize()
		// saving to file
499
		data, marshallErr := tracker.MarshallResponse(gzTorrentGroup)
500
501
		if marshallErr != nil {
			logthis.Error(errors.Wrap(marshallErr, errorWritingJSONMetadata), logthis.NORMAL)
502
		} else {
503
504
505
506
507
508
509
510
511
512
513
514
515
516
			// writing torrent group metadata to target folder
			if e := ioutil.WriteFile(filepath.Join(destination, tm.Tracker+" - "+trackerTGroupMetadataFile), data, 0666); e != nil {
				logthis.Error(errors.Wrap(e, errorWritingJSONMetadata), logthis.NORMAL)
			} else {
				logthis.Info(infoTorrentGroupMetadataSaved, logthis.VERBOSE)
			}
		}

		if c.General.FullMetadataRetrieval {
			// getting collages
			var allCollages []tracker.CollageInfo
			allCollages = append(allCollages, gzTorrentGroup.Group.Collages...)
			allCollages = append(allCollages, gzTorrentGroup.Group.PersonalCollages...)
			for _, c := range allCollages {
517
				gzCollage, err := t.GetCollage(c.ID)
518
				if err != nil {
519
					logthis.Info(fmt.Sprintf(errorRetrievingCollageInfo, c.ID), logthis.NORMAL)
520
521
522
523
					continue
				}
				gzCollage.Anonymize()
				// saving to file
524
				collageData, collageErr := tracker.MarshallResponse(gzCollage)
525
526
527
528
				if collageErr != nil {
					logthis.Error(errors.Wrap(collageErr, errorWritingJSONMetadata), logthis.NORMAL)
				} else {
					// writing collage metadata to target folder
529
					if e := ioutil.WriteFile(filepath.Join(destination, tm.Tracker+" - "+fmt.Sprintf(trackerCollageMetadataFile, gzCollage.CollageCategoryName, c.ID)), collageData, 0666); e != nil {
530
531
						logthis.Error(errors.Wrap(e, errorWritingJSONMetadata), logthis.NORMAL)
					} else {
532
						logthis.Info(fmt.Sprintf(infoCollageMetadataSaved, c.ID), logthis.VERBOSE)
533
534
535
					}
				}
			}
536
537
538
539
540
		}
	}

	// get artist info
	for _, a := range tm.Artists {
541
542
543
		if a.Role == "Main" || c.General.FullMetadataRetrieval {
			gzArtist, err := t.GetArtist(a.ID)
			if err != nil {
544
				logthis.Info(fmt.Sprintf(errorRetrievingArtistInfo+": %s", a.ID, err.Error()), logthis.NORMAL)
545
546
				continue
			}
547
			artistsJSON, err := tracker.MarshallResponse(gzArtist)
user's avatar
user committed
548
			if err != nil {
549
550
551
552
553
				logthis.Info(fmt.Sprintf(errorRetrievingArtistInfo, a.ID), logthis.NORMAL)
				continue
			}
			// write tracker artist metadata to target folder
			// making sure the artistInfo.name+jsonExt is a valid filename
user's avatar
user committed
554
			if e := ioutil.WriteFile(filepath.Join(destination, t.Name+" - Artist ("+a.Role+") "+a.Name+jsonExt), artistsJSON, 0666); e != nil {
555
556
557
558
				logthis.Error(errors.Wrap(e, errorWritingJSONMetadata), logthis.NORMAL)
			} else {
				logthis.Info(fmt.Sprintf(infoArtistMetadataSaved, a.Role, a.Name), logthis.VERBOSE)
			}
559
560
561
		}
	}
	// generate blank user metadata json
562
	if err := tm.WriteUserJSON(destination); err != nil {
563
		logthis.Error(errors.Wrap(err, errorGeneratingUserMetadataJSON), logthis.NORMAL)
564
565
566
	}

	// download tracker cover to target folder
567
	if err := tm.SaveCover(parentFolder); err != nil {
568
		logthis.Error(errors.Wrap(err, errorDownloadingTrackerCover), logthis.NORMAL)
569
	} else {
570
		logthis.Info(infoCoverSaved, logthis.VERBOSE)
571
	}
572
	logthis.Info(fmt.Sprintf(infoAllMetadataSaved, t.Name, filepath.Base(parentFolder)), logthis.VERBOSE)
573
574
575
	return nil
}

576
func (tm *TrackerMetadata) SaveCover(releaseFolder string) error {
577
578
579
	if tm.CoverURL == "" {
		return errors.New("unknown image url")
	}
580
	filename := filepath.Join(releaseFolder, MetadataDir, tm.Tracker+" - "+trackerCoverFile+filepath.Ext(tm.CoverURL))
581

582
	if fs.FileExists(filename) {
583
584
585
		// already downloaded, or exists in folder already: do nothing
		return nil
	}
user's avatar
user committed
586
587
588

	client := http.Client{Timeout: time.Second * 5}
	response, err := client.Get(tm.CoverURL)
589
590
591
592
593
594
595
596
597
598
599
600
601
	if err != nil {
		return err
	}
	defer response.Body.Close()
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = io.Copy(file, response.Body)
	return err
}

602
func (tm *TrackerMetadata) HTMLDescription() string {
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
	// TODO use HTML template directly!!

	if tm.Title == "" {
		return "No metadata found"
	}
	// artists
	// TODO separate main from guests
	artists := ""
	for i, a := range tm.Artists {
		artists += a.Name
		if i != len(tm.Artists)-1 {
			artists += ", "
		}
	}
	// tracklist
	tracklist := ""
	for _, t := range tm.Tracks {
		tracklist += t.String() + "\n"
	}
	// compile remaster info
	remaster := ""
	if tm.EditionName != "" || tm.EditionYear != 0 {
		remaster = fmt.Sprintf(remasterTemplate, tm.RecordLabel, tm.CatalogNumber, tm.EditionYear, tm.EditionName)
	}
	// lineage
	lineage := ""
	for _, l := range tm.Lineage {
		lineage += fmt.Sprintf("**%s**: %s\n", l.Source, l.LinkOrDescription)
	}
	// alive
	isAlive := "still registered"
	if !tm.IsAlive {
		isAlive = "unregistered"
	}
	// general output
	md := fmt.Sprintf(mdTemplate, artists, tm.Title, tm.OriginalYear, tm.CoverURL, strings.Join(tm.Tags, ", "),
		tm.ReleaseType, tm.RecordLabel, tm.CatalogNumber, tm.Source, remaster, tm.Format, tm.Quality, tracklist,
		lineage, time.Now().Format("2006-01-02 15:04"), isAlive, tm.Tracker, tm.ReleaseURL)
	return string(blackfriday.Run([]byte(md)))
}

644
func (tm *TrackerMetadata) TextDescription(fancy bool) string {
645
	artists := make([]string, len(tm.Artists))
user's avatar
user committed
646
647
	for i, a := range tm.Artists {
		artists[i] = a.Name
648
649
650
651
652
653
654
655
656
	}
	artistNames := strings.Join(artists, ", ")

	titleStyle := ""
	reset := ""
	style := func(x string) string { return x }
	if fancy {
		titleStyle = ansi.ColorCode("green+hub")
		reset = ansi.ColorCode("reset")
657
		style = ansi.ColorFunc("blue+hb")
658
659
660
	}
	fullTitle := titleStyle + artistNames + " - " + tm.Title + reset

antipathique's avatar
antipathique committed
661
662
663
664
665
	year := tm.OriginalYear
	if tm.EditionYear != 0 {
		year = tm.EditionYear
	}

666
667
668
	return fmt.Sprintf(txtDescription,
		fullTitle,
		style(tm.ReleaseType),
antipathique's avatar
antipathique committed
669
		style(fmt.Sprintf("%d", year)),
670
671
672
673
674
675
676
677
678
		style(strings.Join(tm.Tags, ", ")),
		style(tm.RecordLabel),
		style(tm.CatalogNumber),
		style(tm.EditionName),
		style(fmt.Sprintf("%d", len(tm.Tracks))),
		style(tm.SourceFull),
		style(tm.Format),
		style(tm.Quality),
		style(tm.Tracker),
antipathique's avatar
antipathique committed
679
680
		style(fmt.Sprintf("%d", tm.ID)),
		style(fmt.Sprintf("%d", tm.GroupID)),
681
682
		style(tm.ReleaseURL),
		style(tm.CoverURL),
683
		style(humanize.IBytes(tm.Size)),
684
	)
685
686
}

687
688
689
690
691
692
693
func getAudioInfo(f string) (string, string, error) {
	stream, err := flac.ParseFile(f)
	if err != nil {
		return "", "", errors.Wrap(err, "could not get FLAC information")
	}
	defer stream.Close()

694
695
696
697
698
699
700
701
702
	var format string
	switch stream.Info.BitsPerSample {
	case 16:
		format = "FLAC"
	case 24:
		format = "FLAC24"
	default:
		logthis.Info("Found bit depth:"+strconv.Itoa(int(stream.Info.BitsPerSample)), logthis.NORMAL)
		format = "FLACXX"
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
	}

	var sampleRate string
	if stream.Info.SampleRate%1000 == 0 {
		sampleRate = fmt.Sprintf("%d", int32(stream.Info.SampleRate/1000))
	} else {
		sampleRate = fmt.Sprintf("%.1f", float32(stream.Info.SampleRate)/1000)
	}
	return format, sampleRate, nil
}

func getFullAudioFormat(f string) (string, error) {
	format, sampleRate, err := getAudioInfo(f)
	if err != nil {
		return "", err
	}
	if format == "FLAC" && sampleRate == "44.1" {
		return format, nil
	}
	return fmt.Sprintf("%s-%skHz", format, sampleRate), nil
}

func (tm *TrackerMetadata) GeneratePath(folderTemplate, releaseFolder string) string {
726
727
728
729
730
	if folderTemplate == "" {
		return tm.FolderName
	}

	// usual edition specifiers, shortened
731
	editionName := tracker.ShortEdition(tm.EditionName)
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747

	// identifying info
	var idElements []string
	if tm.EditionYear != 0 && tm.EditionYear != tm.OriginalYear {
		idElements = append(idElements, fmt.Sprintf("%d", tm.EditionYear))
	}
	if editionName != "" {
		idElements = append(idElements, editionName)
	}
	// adding catalog number, or if not specified, the record label
	if tm.CatalogNumber != "" {
		idElements = append(idElements, tm.CatalogNumber)
	} else if tm.RecordLabel != "" {
		idElements = append(idElements, tm.RecordLabel)
	} // TODO when we have neither catnum nor label

748
749
750
751
752
	id := strings.Join(idElements, ", ")
	if id == "" {
		id = "Unknown"
	}

753
	var releaseTypeExceptAlbum string
754
	if tm.ReleaseType != tracker.ReleaseAlbum {
755
		// adding release type if not album
756
		releaseTypeExceptAlbum = tm.ReleaseType
757
758
	}

759
	quality := tracker.ShortEncoding(tm.Quality)
760
761
762
	if quality == "FLAC" || quality == "FLAC24" {
		// get one music file then find sample rate
		//firstTrackFilename := filepath.Join(releaseFolder, tm.Tracks[0].Title)
user's avatar
user committed
763
		firstTrackFilename := music.GetFirstFLACFound(releaseFolder)
764
765
		fullFormat, err := getFullAudioFormat(firstTrackFilename)
		if err != nil {
766
			logthis.Error(err, logthis.VERBOSEST)
767
768
769
770
771
		} else {
			quality = fullFormat
		}
	}

772
773
774
	r := strings.NewReplacer(
		"$id", "{{$id}}",
		"$a", "{{$a}}",
775
776
		"$ma", "{{$ma}}",
		"$c", "{{$c}}",
777
778
779
780
781
782
783
784
		"$t", "{{$t}}",
		"$y", "{{$y}}",
		"$f", "{{$f}}",
		"$s", "{{$s}}",
		"$l", "{{$l}}",
		"$n", "{{$n}}",
		"$e", "{{$e}}",
		"$g", "{{$g}}",
785
786
		"$r", "{{$r}}",
		"$xar", "{{$xar}}",
787
788
789
790
791
		"{", "ÆÆ", // otherwise golang's template throws a fit if '{' or '}' are in the user pattern
		"}", "¢¢", // assuming these character sequences will probably not cause conflicts.
	)

	// replace with all valid epub parameters
792
	tmpl := fmt.Sprintf(`{{$c := %q}}{{$ma := %q}}{{$a := %q}}{{$y := "%d"}}{{$t := %q}}{{$f := %q}}{{$s := %q}}{{$g := %q}}{{$l := %q}}{{$n := %q}}{{$e := %q}}{{$id := %q}}{{$r := %q}}{{$xar := %q}}%s`,
793
794
795
		fs.SanitizePath(tm.Category),
		fs.SanitizePath(tm.MainArtistAlias),
		fs.SanitizePath(tm.MainArtist),
796
		tm.OriginalYear,
797
		fs.SanitizePath(tm.Title),
798
		quality,
799
		tm.Source,
800
		tm.SourceFull, // source with indicator if 100%/log/cue or Silver/gold & lossy web/master
801
		fs.SanitizePath(tm.RecordLabel),
802
		tm.CatalogNumber,
803
804
		fs.SanitizePath(editionName), // edition
		fs.SanitizePath(id),          // identifying info
805
806
		tm.ReleaseType,
		releaseTypeExceptAlbum,
807
808
809
810
811
812
813
814
		r.Replace(folderTemplate))

	var doc bytes.Buffer
	te := template.Must(template.New("hop").Parse(tmpl))
	if err := te.Execute(&doc, nil); err != nil {
		return tm.FolderName
	}
	newName := strings.TrimSpace(doc.String())
815
816
817
818
819
	// trim spaces around all internal folder names
	var trimmedParts = strings.Split(newName, "/")
	for i, part := range trimmedParts {
		trimmedParts[i] = strings.TrimSpace(part)
	}
820
821
822
823
824
	// recover brackets
	r2 := strings.NewReplacer(
		"ÆÆ", "{",
		"¢¢", "}",
	)
825
	return r2.Replace(strings.Join(trimmedParts, "/"))
826
827
}

828
829
func (tm *TrackerMetadata) WriteUserJSON(destination string) error {
	userJSON := filepath.Join(destination, userMetadataJSONFile)
830
	if fs.FileExists(userJSON) {
831
		logthis.Info("user metadata JSON already exists", logthis.VERBOSE)
832
833
834
835
836
837
838
839
840
841
		return nil
	}
	// save as blank JSON, with no values, for the user to force metadata values if needed.
	blank := TrackerMetadata{}
	blank.Artists = append(blank.Artists, TrackerMetadataArtist{})
	blank.Tracks = append(blank.Tracks, TrackerMetadataTrack{})
	blank.Lineage = append(blank.Lineage, TrackerMetadataLineage{})
	blank.HasLog = tm.HasLog
	blank.HasCue = tm.HasCue
	blank.IsScene = tm.IsScene
842
843
844
	blank.MainArtist = tm.MainArtist
	blank.MainArtistAlias = tm.MainArtistAlias
	blank.Category = tm.Category
845
846
847
848
849
850
851
	metadataJSON, err := json.MarshalIndent(blank, "", "    ")
	if err != nil {
		return err
	}
	return ioutil.WriteFile(userJSON, metadataJSON, 0644)
}

852
853
func (tm *TrackerMetadata) UpdateUserJSON(destination, mainArtist, mainArtistAlias, category string) error {
	userJSON := filepath.Join(destination, userMetadataJSONFile)
854
	if !fs.FileExists(userJSON) {
855
856
		// try to create the file
		if err := tm.WriteUserJSON(destination); err != nil {
857
			return errors.New("user metadata JSON does not already exist and could not be written")
858
859
860
861
862
863
		}
	}

	// loading user metadata file
	userJSONBytes, err := ioutil.ReadFile(userJSON)
	if err != nil {
864
		return errors.New("could not read user JSON")
865
866
867
	}
	var userInfo *TrackerMetadata
	if unmarshalErr := json.Unmarshal(userJSONBytes, &userInfo); unmarshalErr != nil {
868
		logthis.Info("error parsing torrent info JSON", logthis.NORMAL)
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
		return nil
	}
	// overwriting select values
	// NOTE: since we are sorting from the downloads folder to the library, there is no reason why these values would have been set by the user
	// So nothing should be lost.
	userInfo.MainArtist = mainArtist
	userInfo.MainArtistAlias = mainArtistAlias
	userInfo.Category = category
	// write back
	metadataJSON, err := json.MarshalIndent(userInfo, "", "    ")
	if err != nil {
		return err
	}
	return ioutil.WriteFile(userJSON, metadataJSON, 0644)
}

885
886
func (tm *TrackerMetadata) LoadUserJSON(parentFolder string) error {
	userJSON := filepath.Join(parentFolder, userMetadataJSONFile)
887
	if !fs.FileExists(userJSON) {
888
		logthis.Info("user metadata JSON does not exist", logthis.VERBOSEST)
889
890
891
892
893
		return nil
	}
	// loading user metadata file
	userJSONBytes, err := ioutil.ReadFile(userJSON)
	if err != nil {
894
		return errors.New("could not read user JSON")
895
896
897
	}
	var userInfo *TrackerMetadata
	if unmarshalErr := json.Unmarshal(userJSONBytes, &userInfo); unmarshalErr != nil {
user's avatar
user committed
898
		logthis.Error(errors.Wrap(unmarshalErr, "error parsing torrent info JSON: "+userJSON), logthis.NORMAL)
899
900
901
902
903
904
905
906
907
		return nil
	}
	//  overwrite tracker values if non-zero value found
	s := reflect.ValueOf(tm).Elem()
	s2 := reflect.ValueOf(userInfo).Elem()
	for i := 0; i < s.NumField(); i++ {
		f := s.Field(i)
		f2 := s2.Field(i)
		if f.Type().String() == "string" && f2.String() != "" {
908
			f.Set(f2)
909
910
		}
		if (f.Type().String() == "int" || f.Type().String() == "int64") && f2.Int() != 0 {
911
			f.Set(f2)
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
		}
		// NOTE: nothing is done with boolean values. Hard to say if the value read is the default one or user-defined.
	}
	// more complicated types
	if len(userInfo.Tags) != 0 {
		// TODO or concatenate lists?
		tm.Tags = userInfo.Tags
	}
	// if artists are defined which are not blank
	if len(userInfo.Artists) != 0 {
		if userInfo.Artists[0].Name != "" {
			tm.Artists = userInfo.Artists
		}
	}
	if len(userInfo.Tracks) != 0 {
		if userInfo.Tracks[0].Title != "" {
			tm.Tracks = userInfo.Tracks
		}
	}
	if len(userInfo.Lineage) != 0 {
		if userInfo.Lineage[0].Source != "" {
			tm.Lineage = userInfo.Lineage
		}
	}
	return nil
}

func (tm *TrackerMetadata) Release() *Release {
	r := &Release{Tracker: tm.Tracker, Timestamp: time.Now()}
	// for now, using artists, composers, "with" categories
	for _, a := range tm.Artists {
		r.Artists = append(r.Artists, a.Name)
	}
	r.Title = tm.Title
	if tm.EditionYear != 0 {
		r.Year = tm.EditionYear
	} else {
		r.Year = tm.OriginalYear
	}
	r.ReleaseType = tm.ReleaseType
	r.Format = tm.Format
	r.Quality = tm.Quality
	r.HasLog = tm.HasLog
	r.HasCue = tm.HasCue
	r.IsScene = tm.IsScene
	r.Source = tm.Source
	r.Tags = tm.Tags
	// r.url =
	// r.torrentURL =
	r.TorrentID = fmt.Sprintf("%d", tm.ID)
	r.GroupID = fmt.Sprintf("%d", tm.GroupID)
	// r.TorrentFile =
	r.Size = tm.Size
	r.Folder = tm.FolderName
	r.LogScore = tm.LogScore
	return r
}

// IsWellSeeded if it has more than minimumSeeders.
func (tm *TrackerMetadata) IsWellSeeded() bool {
	return tm.CurrentSeeders >= minimumSeeders
}