cli.go 13.2 KB
Newer Older
1
2
3
package main

import (
4
	"encoding/json"
antipathique's avatar
antipathique committed
5
	"fmt"
6
	"os"
7
	"os/exec"
8
	"path/filepath"
9
	"strings"
antipathique's avatar
antipathique committed
10

11
	docopt "github.com/docopt/docopt-go"
12
	"github.com/pkg/errors"
13
14
	"gitlab.com/catastrophic/assistance/fs"
	"gitlab.com/catastrophic/assistance/intslice"
15
	"gitlab.com/catastrophic/assistance/logthis"
16
	"gitlab.com/catastrophic/assistance/strslice"
17
	"gitlab.com/passelecasque/varroa"
18
19
20
)

const (
21
	varroaUsage = `
antipathique's avatar
antipathique committed
22
23
	_  _ ____ ____ ____ ____ ____    _  _ _  _ ____ _ ____ ____
	|  | |__| |__/ |__/ |  | |__|    |\/| |  | [__  | |    |__|
24
	 \/  |  | |  \ |  \ |__| |  |    |  | |__| ___] | |___ |  | (%s)
antipathique's avatar
antipathique committed
25
26
27
28
29
30
31
32
33
34
35
36


Description:

	varroa musica is a personal assistant for your favorite tracker.

	It can:
	- snatch, and autosnatch torrents with quite thorough filters
	- monitor your stats and generate graphs
	- host said graphs on its embedded webserver or on Gitlab Pages
	- save and update all snatched torrents metadata
	- be remotely controlled from your browser with a GreaseMonkey script.
37
38
	- send notifications to your Android device or to a given IRC user 
	  about stats and snatches.
user's avatar
user committed
39
	- check local logs against logchecker.php
40
41
	- sort downloads, export them to your library, automatically rename
	  folders using tracker metadata
42
43
	- mount a read-only FUSE filesystem exposing your downloads or library
	  using tracker metadata
antipathique's avatar
antipathique committed
44
45
46
47
48
49
50
51
52
53
54

Daemon Commands:

	The daemon is used for autosnatching, stats monitoring and hosting,
	and remotely triggering snatches from the GM script or any
	pyWhatAuto remote (including the Android App).

	start:
		starts the daemon.
	stop:
		stops it.
antipathique's avatar
antipathique committed
55
56
	uptime:
		shows how long it has been running.
57
58
	status
		returns information about the daemon status.
antipathique's avatar
antipathique committed
59
60
61
62
63
64
65

Commands:

	stats:
		generates the stats immediately based on currently saved
		history.
	refresh-metadata:
66
67
68
69
70
		retrieves all metadata for releases with the given local
		path, updating the files that were downloaded when they
		were first snatched (allows updating local metadata if a
		torrent has been edited since upload).
	refresh-metadata-by-id:
antipathique's avatar
antipathique committed
71
72
73
74
75
76
77
78
79
		retrieves all metadata for all torrents with IDs given as
		arguments, updating the files that were downloaded when they
		were first snatched (allows updating local metadata if a
		torrent has been edited since upload).
	check-log:
		upload a given log file to the tracker's logchecker.php and
		returns its score.
	snatch:
		snatch all torrents with IDs given as arguments.
80
81
	info:
		output info about the torrent IDs given as argument.
antipathique's avatar
antipathique committed
82
83
84
	backup:
		backup user files (stats, history, configuration file) to a
		timestamped zip file. Automatically triggered every day.
85
	downloads search:
86
		return all known downloads on which an artist has worked.
87
88
89
	downloads metadata:
		return information about a specific download. Takes downloads
		db ID as argument.
90
	downloads sort:
user's avatar
user committed
91
92
93
94
95
		sort all unsorted downloads, or sort a specific release
		(identified by its path). sorting allows you to tag which
		release to keep and which to only seed; selected downloads
		can be exported to an external folder.
	downloads sort-id:
antipathique's avatar
antipathique committed
96
97
98
99
		sort all unsorted downloads, or sort a specific release
		(identified by its db ID). sorting allows you to tag which
		release to keep and which to only seed; selected downloads
		can be exported to an external folder.
100
	downloads list:
user's avatar
user committed
101
102
		list all downloads, of filter by state: unsorted, accepted, 
	    exported, rejected.
103
104
105
	downloads clean:
		clean up the downloads directory by moving all empty folders,
		and folders with only tracker metadata, to a dedicated subfolder.
106
107
108
109
	downloads fuse:
		mount a read-only filesystem exposing your downloads using the
		tracker metadata, using the following categories: artists, tags,
		record labels, years. Call 'fusermount -u MOUNT_POINT' to stop.
user's avatar
user committed
110
111
112
	library reorganize:
		renames all releases in the library (including parent folders) 
		using tracker metadata and the user-defined folder template.
113
114
	library fuse:
		similar to downloads fuse, but for your music library.
115
	reseed:
antipathique's avatar
antipathique committed
116
117
		reseed a downloaded release using tracker metadata. Does not check
		the torrent files actually match the contents in the given PATH.
118
	
119
120
Configuration Commands:

121
122
123
124
	show-config:
		displays what varroa has parsed from the configuration file
		(useful for checking the YAML is correctly formatted, and the
		filters are correctly interpreted).
125
126
127
128
129
130
131
132
133
134
	encrypt:
		encrypts your configuration file. The encrypted version can
		be used in place of the plaintext version, if you're
		uncomfortable having passwords lying around in an simple text
		file. You will be prompted for a passphrase which you will
		have to enter again every time you run varroa. Your passwords
		will still be decoded in memory while varroa is up. This
		command does not remove the plaintext version.
	decrypt:
		decrypts your encrypted configuration file.
135
136

Usage:
user's avatar
user committed
137
	varroa (start [--no-daemon]|stop|uptime|status)
138
	varroa stats
139
140
	varroa refresh-metadata <PATH>...
	varroa refresh-metadata-by-id <TRACKER> <ID>...
141
	varroa check-log <TRACKER> <LOG_FILE>
142
	varroa snatch [--fl] <TRACKER> <ID>...
143
	varroa info <TRACKER> <ID>...
144
	varroa backup
145
	varroa show-config
146
	varroa (downloads|dl) (search <ARTIST>|metadata <ID>|sort [--new] [<PATH>...]|sort-id [<ID>...]|list [<STATE>]|clean|fuse <MOUNT_POINT>)
147
	varroa library (fuse <MOUNT_POINT>|reorganize [--simulate|--interactive])
148
	varroa reseed <TRACKER> <PATH>
149
	varroa (encrypt|decrypt)
150
151
152
153
	varroa --version

Options:
 	-h, --help             Show this screen.
user's avatar
user committed
154
	--no-daemon            Starts varroa but without turning it into a daemon. No log will be kept. Ctrl+C to quit.
155
 	--fl                   Use personal Freeleech torrent if available.
156
	--simulate             Simulate library reorganization to show what would be renamed.
157
	--interactive          Library reorganization requires user confirmation for each release if necessary.
158
	--new                  Only sort new releases (ignore previously sorted ones)
159
160
161
162
  	--version              Show version.
`
)

163
164
165
166
167
168
type refreshTarget struct {
	path    string
	tracker string
	id      int
}

169
type varroaArguments struct {
170
171
	builtin                 bool
	start                   bool
user's avatar
user committed
172
	noDaemon                bool
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
	stop                    bool
	uptime                  bool
	status                  bool
	stats                   bool
	refreshMetadata         bool
	refreshMetadataByID     bool
	checkLog                bool
	snatch                  bool
	info                    bool
	backup                  bool
	showConfig              bool
	encrypt                 bool
	decrypt                 bool
	downloadSearch          bool
	downloadInfo            bool
	downloadSort            bool
	downloadSortID          bool
	downloadList            bool
	downloadState           string
	downloadClean           bool
	downloadFuse            bool
	libraryFuse             bool
	libraryReorg            bool
	libraryReorgInteractive bool
	libraryReorgSimulate    bool
	reseed                  bool
	useFLToken              bool
200
	ignoreSorted            bool
201
202
203
204
205
206
207
208
209
	torrentIDs              []int
	logFile                 string
	trackerLabel            string
	paths                   []string
	artistName              string
	mountPoint              string
	requiresDaemon          bool
	canUseDaemon            bool
	toRefresh               []refreshTarget
210
211
}

212
func (b *varroaArguments) parseCLI(osArgs []string) error {
213
	// parse arguments and options
214
	args, err := docopt.Parse(fmt.Sprintf(varroaUsage, varroa.Version), osArgs, true, fmt.Sprintf(varroa.FullVersion, varroa.FullName, varroa.Version), false, false)
215
	if err != nil {
216
		return errors.Wrap(err, varroa.ErrorInfoBadArguments)
217
218
219
220
221
222
223
224
	}
	if len(args) == 0 {
		// builtin command, nothing to do.
		b.builtin = true
		return nil
	}
	// commands
	b.start = args["start"].(bool)
user's avatar
user committed
225
	b.noDaemon = args["--no-daemon"].(bool)
226
	b.stop = args["stop"].(bool)
antipathique's avatar
antipathique committed
227
	b.uptime = args["uptime"].(bool)
228
	b.status = args["status"].(bool)
229
	b.stats = args["stats"].(bool)
user's avatar
user committed
230
	b.reseed = args["reseed"].(bool)
user's avatar
user committed
231
	//b.enhance = args["enhance"].(bool)
232
	b.refreshMetadataByID = args["refresh-metadata-by-id"].(bool)
233
	b.refreshMetadata = args["refresh-metadata"].(bool)
234
	b.checkLog = args["check-log"].(bool)
235
	b.snatch = args["snatch"].(bool)
236
	b.backup = args["backup"].(bool)
237
	b.info = args["info"].(bool)
238
	b.showConfig = args["show-config"].(bool)
239
240
	b.encrypt = args["encrypt"].(bool)
	b.decrypt = args["decrypt"].(bool)
241
	if args["downloads"].(bool) || args["dl"].(bool) {
242
243
244
		b.downloadSearch = args["search"].(bool)
		if b.downloadSearch {
			b.artistName = args["<ARTIST>"].(string)
245
		}
246
		b.downloadInfo = args["metadata"].(bool)
247
		b.downloadSort = args["sort"].(bool)
248
249
250
		if b.downloadSort {
			b.ignoreSorted = args["--new"].(bool)
		}
user's avatar
user committed
251
		b.downloadSortID = args["sort-id"].(bool)
252
		b.downloadList = args["list"].(bool)
253
		b.downloadClean = args["clean"].(bool)
254
		b.downloadFuse = args["fuse"].(bool)
255
	}
256
257
	if args["library"].(bool) {
		b.libraryFuse = args["fuse"].(bool)
258
		b.libraryReorg = args["reorganize"].(bool)
259
		b.libraryReorgSimulate = args["--simulate"].(bool)
260
		b.libraryReorgInteractive = args["--interactive"].(bool)
261
	}
262
	if b.reseed || b.downloadSort {
263
		b.paths = args["<PATH>"].([]string)
user's avatar
user committed
264
		for i, p := range b.paths {
265
			if !fs.DirExists(p) {
266
				return errors.New("target path does not exist")
267
268
			}
			if !varroa.DirectoryContainsMusicAndMetadata(p) {
269
				return fmt.Errorf(varroa.ErrorFindingMusicAndMetadata, p)
270
			}
user's avatar
user committed
271
272
273
			if strings.HasSuffix(p, "/") {
				b.paths[i] = p[:len(p)-1]
			}
274
275
		}
	}
276
	// arguments
277
	if b.refreshMetadataByID || b.snatch || b.downloadInfo || b.downloadSortID || b.info {
278
279
		IDs, ok := args["<ID>"].([]string)
		if !ok {
280
			return errors.New("invalid torrent IDs")
281
		}
282
		b.torrentIDs, err = strslice.ToIntSlice(IDs)
283
		if err != nil {
284
			return errors.New("invalid torrent IDs, must be integers")
285
286
		}
	}
287
	if b.downloadFuse || b.libraryFuse {
288
289
290
291
292
293
		// checking fusermount is available
		_, err := exec.LookPath("fusermount")
		if err != nil {
			return errors.New("fusermount is not available on this system, cannot use the fuse command")
		}

294
		b.mountPoint = args["<MOUNT_POINT>"].(string)
295
		if !fs.DirExists(b.mountPoint) {
296
			return errors.New("fuse mount point does not exist")
297
298
		}

299
		// check it's empty
300
		if isEmpty, err := fs.DirIsEmpty(b.mountPoint); err != nil {
301
			return errors.New("could not open Fuse mount point")
302
		} else if !isEmpty {
303
			return errors.New("fuse mount point is not empty")
304
		}
305
	}
306
	if b.downloadList {
307
308
309
310
		state, ok := args["<STATE>"].(string)
		if ok {
			b.downloadState = state
			if !varroa.IsValidDownloadState(b.downloadState) {
311
				return errors.New("invalid download state, must be among: " + strings.Join(varroa.DownloadFolderStates, ", "))
312
			}
313
314
		}
	}
315
316
317
	if b.snatch {
		b.useFLToken = args["--fl"].(bool)
	}
318
319
	if b.checkLog {
		logPath := args["<LOG_FILE>"].(string)
320
		if !fs.FileExists(logPath) {
321
			return errors.New("invalid log file, does not exist")
322
323
324
		}
		b.logFile = logPath
	}
325
	if b.refreshMetadataByID || b.snatch || b.checkLog || b.info || b.reseed {
326
327
		b.trackerLabel = args["<TRACKER>"].(string)
	}
328

329
330
331
332
333
334
335
	if b.refreshMetadata {
		paths := args["<PATH>"].([]string)
		currentPath, err := os.Getwd()
		if err != nil {
			return err
		}
		for _, p := range paths {
336
			if !fs.DirExists(p) {
337
				return errors.New("target path " + p + " does not exist")
338
339
			}
			if !varroa.DirectoryContainsMusicAndMetadata(p) {
340
				return fmt.Errorf(varroa.ErrorFindingMusicAndMetadata, p)
341
342
343
344
345
346
347
348
349
350
351
352
			}
			// find the parent directory
			root := currentPath
			if filepath.IsAbs(p) {
				root = filepath.Dir(p)
			}
			// load metadata
			d := varroa.DownloadEntry{FolderName: p}
			if err := d.Load(root); err != nil {
				return err
			}
			// get tracker + ids
353
354
			for i := range d.Tracker {
				b.toRefresh = append(b.toRefresh, refreshTarget{path: p, tracker: d.Tracker[i], id: d.TrackerID[i]})
355
356
357
358
			}
		}
	}

359
360
361
	// sorting which commands can use the daemon if it's there but should manage if it is not
	b.requiresDaemon = true
	b.canUseDaemon = true
362
	if b.refreshMetadataByID || b.refreshMetadata || b.snatch || b.checkLog || b.backup || b.stats || b.downloadSearch || b.downloadInfo || b.downloadSort || b.downloadSortID || b.downloadList || b.info || b.downloadClean || b.downloadFuse || b.libraryFuse || b.libraryReorg || b.reseed {
363
364
365
		b.requiresDaemon = false
	}
	// sorting which commands should not interact with the daemon in any case
366
	if b.refreshMetadata || b.backup || b.showConfig || b.decrypt || b.encrypt || b.downloadSearch || b.downloadInfo || b.downloadSort || b.downloadSortID || b.downloadList || b.downloadClean || b.downloadFuse || b.libraryFuse || b.libraryReorg {
367
368
		b.canUseDaemon = false
	}
369
370
	return nil
}
antipathique's avatar
antipathique committed
371

372
func (b *varroaArguments) commandToDaemon() []byte {
373
	out := varroa.IncomingJSON{Site: b.trackerLabel}
antipathique's avatar
antipathique committed
374
	if b.stats {
375
		out.Command = "stats"
antipathique's avatar
antipathique committed
376
377
378
	}
	if b.stop {
		// to cleanly close the unix socket
379
		out.Command = "stop"
antipathique's avatar
antipathique committed
380
	}
antipathique's avatar
antipathique committed
381
382
383
	if b.uptime {
		out.Command = "uptime"
	}
384
385
386
	if b.status {
		out.Command = "status"
	}
387
388
	if b.refreshMetadataByID {
		out.Command = "refresh-metadata-by-id"
389
		out.Args = intslice.ToStringSlice(b.torrentIDs)
antipathique's avatar
antipathique committed
390
391
	}
	if b.snatch {
392
		out.Command = "snatch"
393
		out.Args = intslice.ToStringSlice(b.torrentIDs)
394
		out.FLToken = b.useFLToken
antipathique's avatar
antipathique committed
395
	}
396
397
	if b.info {
		out.Command = "info"
398
		out.Args = intslice.ToStringSlice(b.torrentIDs)
antipathique's avatar
antipathique committed
399
400
	}
	if b.checkLog {
401
402
403
		out.Command = "check-log"
		out.Args = []string{b.logFile}
	}
404
405
	if b.reseed {
		out.Command = "reseed"
406
		out.Args = b.paths
407
	}
408
409
	commandBytes, err := json.Marshal(out)
	if err != nil {
410
		logthis.Error(errors.Wrap(err, "cannot parse command"), logthis.NORMAL)
411
		return []byte{}
antipathique's avatar
antipathique committed
412
	}
413
	return commandBytes
antipathique's avatar
antipathique committed
414
}