Skip to content

Commit 7d55126

Browse files
committed
Merge pull request #1397 from multikatt/ipfs
Ipfs plugin
2 parents 1eb3c3c + cdef5fd commit 7d55126

File tree

4 files changed

+436
-0
lines changed

4 files changed

+436
-0
lines changed

beetsplug/ipfs.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# This file is part of beets.
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining
4+
# a copy of this software and associated documentation files (the
5+
# "Software"), to deal in the Software without restriction, including
6+
# without limitation the rights to use, copy, modify, merge, publish,
7+
# distribute, sublicense, and/or sell copies of the Software, and to
8+
# permit persons to whom the Software is furnished to do so, subject to
9+
# the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be
12+
# included in all copies or substantial portions of the Software.
13+
14+
"""Adds support for ipfs. Requires go-ipfs and a running ipfs daemon
15+
"""
16+
from beets import ui, util, library, config
17+
from beets.plugins import BeetsPlugin
18+
19+
import subprocess
20+
import shutil
21+
import os
22+
import tempfile
23+
24+
25+
class IPFSPlugin(BeetsPlugin):
26+
27+
def __init__(self):
28+
super(IPFSPlugin, self).__init__()
29+
self.config.add({
30+
'auto': True,
31+
})
32+
33+
if self.config['auto']:
34+
self.import_stages = [self.auto_add]
35+
36+
def commands(self):
37+
cmd = ui.Subcommand('ipfs',
38+
help='interact with ipfs')
39+
cmd.parser.add_option('-a', '--add', dest='add',
40+
action='store_true',
41+
help='Add to ipfs')
42+
cmd.parser.add_option('-g', '--get', dest='get',
43+
action='store_true',
44+
help='Get from ipfs')
45+
cmd.parser.add_option('-p', '--publish', dest='publish',
46+
action='store_true',
47+
help='Publish local library to ipfs')
48+
cmd.parser.add_option('-i', '--import', dest='_import',
49+
action='store_true',
50+
help='Import remote library from ipfs')
51+
cmd.parser.add_option('-l', '--list', dest='_list',
52+
action='store_true',
53+
help='Query imported libraries')
54+
cmd.parser.add_option('-m', '--play', dest='play',
55+
action='store_true',
56+
help='Play music from remote libraries')
57+
58+
def func(lib, opts, args):
59+
if opts.add:
60+
for album in lib.albums(ui.decargs(args)):
61+
if len(album.items()) == 0:
62+
self._log.info('{0} does not contain items, aborting',
63+
album)
64+
65+
self.ipfs_add(album)
66+
album.store()
67+
68+
if opts.get:
69+
self.ipfs_get(lib, ui.decargs(args))
70+
71+
if opts.publish:
72+
self.ipfs_publish(lib)
73+
74+
if opts._import:
75+
self.ipfs_import(lib, ui.decargs(args))
76+
77+
if opts._list:
78+
self.ipfs_list(lib, ui.decargs(args))
79+
80+
if opts.play:
81+
self.ipfs_play(lib, opts, ui.decargs(args))
82+
83+
cmd.func = func
84+
return [cmd]
85+
86+
def auto_add(self, session, task):
87+
if task.is_album:
88+
if self.ipfs_add(task.album):
89+
task.album.store()
90+
91+
def ipfs_play(self, lib, opts, args):
92+
from beetsplug.play import PlayPlugin
93+
94+
jlib = self.get_remote_lib(lib)
95+
player = PlayPlugin()
96+
config['play']['relative_to'] = None
97+
player.album = True
98+
player.play_music(jlib, player, args)
99+
100+
def ipfs_add(self, album):
101+
try:
102+
album_dir = album.item_dir()
103+
except AttributeError:
104+
return False
105+
try:
106+
if album.ipfs:
107+
self._log.debug('{0} already added', album_dir)
108+
# Already added to ipfs
109+
return False
110+
except AttributeError:
111+
pass
112+
113+
self._log.info('Adding {0} to ipfs', album_dir)
114+
115+
cmd = "ipfs add -q -r".split()
116+
cmd.append(album_dir)
117+
try:
118+
output = util.command_output(cmd).split()
119+
except (OSError, subprocess.CalledProcessError) as exc:
120+
self._log.error(u'Failed to add {0}, error: {1}', album_dir, exc)
121+
return False
122+
length = len(output)
123+
124+
for linenr, line in enumerate(output):
125+
line = line.strip()
126+
if linenr == length - 1:
127+
# last printed line is the album hash
128+
self._log.info("album: {0}", line)
129+
album.ipfs = line
130+
else:
131+
try:
132+
item = album.items()[linenr]
133+
self._log.info("item: {0}", line)
134+
item.ipfs = line
135+
item.store()
136+
except IndexError:
137+
# if there's non music files in the to-add folder they'll
138+
# get ignored here
139+
pass
140+
141+
return True
142+
143+
def ipfs_get(self, lib, query):
144+
query = query[0]
145+
# Check if query is a hash
146+
if query.startswith("Qm") and len(query) == 46:
147+
self.ipfs_get_from_hash(lib, query)
148+
else:
149+
albums = self.query(lib, query)
150+
for album in albums:
151+
self.ipfs_get_from_hash(lib, album.ipfs)
152+
153+
def ipfs_get_from_hash(self, lib, _hash):
154+
try:
155+
cmd = "ipfs get".split()
156+
cmd.append(_hash)
157+
util.command_output(cmd)
158+
except (OSError, subprocess.CalledProcessError) as err:
159+
self._log.error('Failed to get {0} from ipfs.\n{1}',
160+
_hash, err.output)
161+
return False
162+
163+
self._log.info('Getting {0} from ipfs', _hash)
164+
imp = ui.commands.TerminalImportSession(lib, loghandler=None,
165+
query=None, paths=[_hash])
166+
imp.run()
167+
shutil.rmtree(_hash)
168+
169+
def ipfs_publish(self, lib):
170+
with tempfile.NamedTemporaryFile() as tmp:
171+
self.ipfs_added_albums(lib, tmp.name)
172+
try:
173+
cmd = "ipfs add -q ".split()
174+
cmd.append(tmp.name)
175+
output = util.command_output(cmd)
176+
except (OSError, subprocess.CalledProcessError) as err:
177+
msg = "Failed to publish library. Error: {0}".format(err)
178+
self._log.error(msg)
179+
return False
180+
self._log.info("hash of library: {0}", output)
181+
182+
def ipfs_import(self, lib, args):
183+
_hash = args[0]
184+
if len(args) > 1:
185+
lib_name = args[1]
186+
else:
187+
lib_name = _hash
188+
lib_root = os.path.dirname(lib.path)
189+
remote_libs = lib_root + "/remotes"
190+
if not os.path.exists(remote_libs):
191+
try:
192+
os.makedirs(remote_libs)
193+
except OSError as e:
194+
msg = "Could not create {0}. Error: {1}".format(remote_libs, e)
195+
self._log.error(msg)
196+
return False
197+
path = remote_libs + "/" + lib_name + ".db"
198+
if not os.path.exists(path):
199+
cmd = "ipfs get {0} -o".format(_hash).split()
200+
cmd.append(path)
201+
try:
202+
util.command_output(cmd)
203+
except (OSError, subprocess.CalledProcessError):
204+
self._log.error("Could not import {0}".format(_hash))
205+
return False
206+
207+
# add all albums from remotes into a combined library
208+
jpath = remote_libs + "/joined.db"
209+
jlib = library.Library(jpath)
210+
nlib = library.Library(path)
211+
for album in nlib.albums():
212+
if not self.already_added(album, jlib):
213+
new_album = []
214+
for item in album.items():
215+
item.id = None
216+
new_album.append(item)
217+
added_album = jlib.add_album(new_album)
218+
added_album.ipfs = album.ipfs
219+
added_album.store()
220+
221+
def already_added(self, check, jlib):
222+
for jalbum in jlib.albums():
223+
if jalbum.mb_albumid == check.mb_albumid:
224+
return True
225+
return False
226+
227+
def ipfs_list(self, lib, args):
228+
fmt = config['format_album'].get()
229+
try:
230+
albums = self.query(lib, args)
231+
except IOError:
232+
ui.print_("No imported libraries yet.")
233+
return
234+
235+
for album in albums:
236+
ui.print_(format(album, fmt), " : ", album.ipfs)
237+
238+
def query(self, lib, args):
239+
rlib = self.get_remote_lib(lib)
240+
albums = rlib.albums(args)
241+
return albums
242+
243+
def get_remote_lib(self, lib):
244+
lib_root = os.path.dirname(lib.path)
245+
remote_libs = lib_root + "/remotes"
246+
path = remote_libs + "/joined.db"
247+
if not os.path.isfile(path):
248+
raise IOError
249+
return library.Library(path)
250+
251+
def ipfs_added_albums(self, rlib, tmpname):
252+
""" Returns a new library with only albums/items added to ipfs
253+
"""
254+
tmplib = library.Library(tmpname)
255+
for album in rlib.albums():
256+
try:
257+
if album.ipfs:
258+
self.create_new_album(album, tmplib)
259+
except AttributeError:
260+
pass
261+
return tmplib
262+
263+
def create_new_album(self, album, tmplib):
264+
items = []
265+
for item in album.items():
266+
try:
267+
if not item.ipfs:
268+
break
269+
except AttributeError:
270+
pass
271+
# Clear current path from item
272+
item.path = '/ipfs/{0}/{1}'.format(album.ipfs,
273+
os.path.basename(item.path))
274+
275+
item.id = None
276+
items.append(item)
277+
if len(items) < 1:
278+
return False
279+
self._log.info("Adding '{0}' to temporary library", album)
280+
new_album = tmplib.add_album(items)
281+
new_album.ipfs = album.ipfs
282+
new_album.store()

docs/plugins/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Each plugin has its own set of options that can be defined in a section bearing
5050
importfeeds
5151
info
5252
inline
53+
ipfs
5354
keyfinder
5455
lastgenre
5556
lastimport
@@ -130,6 +131,7 @@ Interoperability
130131
----------------
131132

132133
* :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks.
134+
* :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs.
133135
* :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library
134136
changes.
135137
* :doc:`play`: Play beets queries in your music player.

docs/plugins/ipfs.rst

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
IPFS Plugin
2+
===========
3+
4+
The ``ipfs`` plugin makes it easy to share your library and music with friends.
5+
The plugin uses `ipfs`_ for storing the libraries and the file content.
6+
7+
.. _ipfs: http://ipfs.io/
8+
9+
Installation
10+
------------
11+
12+
This plugin requires `go-ipfs`_ running as a daemon and that the ipfs command is
13+
in the users $PATH.
14+
15+
.. _go-ipfs: https://github.com/ipfs/go-ipfs
16+
17+
Enable the ``ipfs`` plugin in your configuration (see :ref:`using-plugins`).
18+
19+
Usage
20+
-----
21+
22+
To add albums to ipfs, making them shareable, use the -a or --add flag. If used
23+
without arguments it will add all albums in the local library. When added all
24+
items and albums will get a ipfs entry in the database containing the hash of
25+
that specific file/folder. Newly imported albums will be added automatically to
26+
ipfs unless set not to do so, see the configuration section below.
27+
28+
These hashes can then be given to a friend and they can ``get`` that album from
29+
ipfs and import it to beets using the -g or --get flag.
30+
If the argument passed to the -g flag isn't an ipfs hash it'll be used as a
31+
query instead, getting all albums matching the query.
32+
33+
Using the -p or --publish flag a copy of the local library will be
34+
published to ipfs. Only albums/items with ipfs records in the database will
35+
published, and local paths will be stripped from the library. A hash of the
36+
library will be returned to the user.
37+
38+
A friend can then import this remote library by using the -i or --import flag.
39+
To tag an imported library with a specific name by passing a name as the second
40+
argument to -i, after the hash.
41+
The content of all remote libraries will be combined into an additional library
42+
as long as the content doesn't already exist in the joined library.
43+
44+
When remote libraries has been imported you can search them by using the -l or
45+
--list flag. The hash of albums matching the query will be returned, this can
46+
then be used with -g to fetch and import the album to the local library.
47+
48+
Ipfs can be mounted as a FUSE file system. This means that music in a remote
49+
library can be streamed directly, without importing them to the local library
50+
first. If the /ipfs folder is mounted then matching queries will be sent to the
51+
:doc:`/plugins/play` using the -m or --play flag.
52+
53+
Configuration
54+
-------------
55+
56+
The ipfs plugin will automatically add imported albums to ipfs and add those hashes
57+
to the database. This can be turned off by setting the ``auto`` option in the
58+
ipfs section of the config to ``no``.

0 commit comments

Comments
 (0)