New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mb web interface #2327
Mb web interface #2327
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that the server only runs while importing, however, how will it behave when port 8000 is used by another process (e.g., if Picard is also running)?
Also, new code should generally be accompanied by tests too.
beets/ui/commands.py
Outdated
@@ -545,7 +545,11 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, | |||
assert not singleton | |||
return importer.action.TRACKS | |||
elif sel == u'e': | |||
return importer.action.MANUAL | |||
ans = ui.input_("Use MB web interface?") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"MB" should probably be spelled out.
beets/util/mb_tagger_server.py
Outdated
@@ -0,0 +1,22 @@ | |||
import socket |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should add a copyright blurb, encoding info, and docstring to the file as well. (See other files.)
beets/util/mb_tagger_server.py
Outdated
import socket | ||
|
||
|
||
class Server: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember to include a docstring.
beets/util/mb_tagger_server.py
Outdated
self.port = port | ||
self.start = None | ||
|
||
def start_server(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstring…
beets/util/mb_tagger_server.py
Outdated
self.start.bind((self.host, self.port)) | ||
self.start.listen(1) | ||
|
||
def listen(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… and docstring here too.
Hey! Thanks for forging right ahead on this. Can we chat a little about the design before you finish up? In particular, I think this should be a plugin. Plugins can add options to the importer action prompt, so we can directly insert an option to launch the browser interface from there. This will avoid bothering people who don't need that option—for example, when running beets in an ssh instance on a remote server. Do you have ideas about how you'll finish up the implementation? For example:
|
Your last point is exactly what I thought. It's just a tiny http server, so there is no need to use Flask. |
Oh yeah! I also meant to add that the |
By the way, I was wondering why do you use Enum in the project? As far as I'm concerned, enum was added in the 3.4 version and it isn't supported in the 2.* version of python. |
On 2.7, we use enum34, the backport available on PyPI. |
This reverts commit 4ffc680.
Hi, @sampsyo. How do you set up plugins? I have created But I am getting ImportError: |
Hi! The easiest way is probably just to use your existing checkout of the beets source (which I'm presuming you've "installed" with |
That's what I did. I have a question about stages: can I just use threading instead of it? Or is it necessary to use due to beets architecture? |
It's probably a better idea to use the special beets hooks for anything that can use them. But what were you imagining to do by using threading directly? I'd be happy to discuss in more detail if you have a specific proposal. For this particular feature, however, I believe everything will need to take place on the "UI thread." That is, it should happen synchronously while the user is considering a single album—I can't see any obvious way to add parallelism here. |
I thought about running server in the separate thread, so the user can always choose another release. If the server is running in the UI thread you must rerun server every time you want to pick different track. |
OK, I see. So the server would always be running in the background, but it would only be "meaningful" if there's some release currently being considered in the UI stage. Is that right? That could make sense, although it could be a bit more complex than starting the server only when a choice is needed. This could be accomplished by starting the server in a background thread when the importer starts and shutting it down when it finishes. |
Yep, that's right. Do you think it would be an appropriate solution? |
Sure! I'd advocate for whichever solution you find leads to the simplest code—the amount of functionality doesn't seem that different, overall. |
Some initial code: https://github.com/tigranl/beets-web-tagger |
Oh, very nifty! Thanks for sending this along. It looks like you have a little more coding to do (or, if I'm wrong, please feel free to summarize where you currently are). It also looks like the standard-library HTTPServer might be useful here? |
There still is some coding to do. I need to figure out how to pass user's 'Look up' choice to |
Cool! For what it's worth, the function should probably return an |
Hi, @sampsyo. As you know, threads in python are implemented using GIL, so there is no way to run two threads simultaneously. But in my case I need to run server thread and beet main thread. So, I guess, the best idea would be to run server every time user wants to choose a track, or to use |
Hi! The GIL actually shouldn't be a problem here—it only becomes an issue if you want code to run faster by taking advantage of multiple cores. It won't stop you from running a web server on one thread and simultaneously doing other beets stuff in the other thread. The web server thread can be waiting for connections, for example, while the other thread makes progress. |
Should I create two separate threads with |
Well, you don't actually need two threads—the main code is already running its own thread (implicitly). You just need to fork, and then eventually join, the second thread. Here's a tiny example, at the risk of being redundant: import threading
import time
class MyThread(threading.Thread):
def run(self):
print('start thread')
time.sleep(2)
print('end thread')
def main():
t = MyThread()
print('before start')
t.start()
t.join()
print('after end')
if __name__ == '__main__':
main() |
Hi! Merry Christmas and a Happy New Year! 🎄 |
Happy holidays to you too! Sure; I'd be happy to help! What's going wrong with that code? Is there anything I should try running locally, or maybe an error message I should look for? |
Try to run it locally. |
OK—specifically, I should copy that gist into my beetsplug folder, enable the plugin, import something, and type Any other context about what's going wrong? |
Yes, after adding socket send function it now works as expected. But I'm not sure about |
Great! A single-file plugin is just fine in this case. Python is telling you this won't work: TerminalImportSession._get_plugin_choices(task) because that's an instance method, not a static method, so it can't be called on the class itself. To make this work cleanly, we may want to extend the interface in the importer itself. It would be nice if the plugin didn't have to explicitly invoke the importer choice logic—if it could just return the ID in question and let the importer take care of the rest. That would avoid the whole problem of invoking stuff in I'll look into this right now. |
OK, I've pushed a bit of refactoring that should make this easy! Now, the built-in option for searching by ID works using exactly the same functionality as a plugin. See this function here: https://github.com/beetbox/beets/blob/master/beets/ui/commands.py#L692-L707 That's a callback from a PromptChoice. As you can see, it just returns a |
f81066c
to
f3b30a9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, looking good so far! This has made a lot of progress.
Before this can be merged, it will need more comments in the code and a documentation page. If you like, though, we can call this GCI task done.
beetsplug/web_tagger.py
Outdated
from beets import ui | ||
|
||
|
||
PORT = 8000 # Temporary for PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be configurable.
beetsplug/web_tagger.py
Outdated
else: | ||
break | ||
parsed = urlparse.urlparse(url) | ||
return str(urlparse.parse_qs(parsed.query)['id'][0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments for this funky parsing code would be really helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this function very much. Any ideas how to rewrite? I am thinking of using regexp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, a regular expression would make sense. Can you give me an idea of what the code's supposed to do (it's a little hard to read without context)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It retrieves Musicbrainz id from data received from the socket. Function makes the list of received bytes and picks up the first value (which is a GET method), then using for loop it catches URI, and finally parses id parameter with urlparse
.
I hope my explanation makes things clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, got it! It seems like the right thing to do is to use a real HTTP parser then. Here's one way to do it, if you want to avoid using a full HTTPServer: http://stackoverflow.com/a/5964334/39182
beetsplug/web_tagger.py
Outdated
self.register_listener('before_choose_candidate', self.prompt) | ||
self.server = Server() | ||
self.server.start() | ||
self.server.join() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why the server is started and then joined at plugin startup time?
beetsplug/web_tagger.py
Outdated
def choice(self, session, task): | ||
artist = ui.input_('Artist:') | ||
realise = ui.input_('Album:') | ||
track = ui.input_('Track:') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should extract these from the task instead of prompting for them.
(Also, realise
-> release
.)
beetsplug/web_tagger.py
Outdated
'realise': realise, | ||
} | ||
url = 'http://musicbrainz.org/taglookup?{0}'.format(urlencode(query)) | ||
ui.print_("Choose your tracks and click 'tagger' button to add:") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably end in a "." instead of a ":" because there's no prompt here.
beetsplug/web_tagger.py
Outdated
_, _, proposal, _ = autotag.tag_album(task.items, search_ids=id_choice) | ||
return proposal | ||
else: | ||
return autotag.tag_item(task.item, search_ids=id_choice) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I'm glad the new API works.
beetsplug/web_tagger.py
Outdated
track = ui.input_('Track:') | ||
if not (artist, realise, track): | ||
ui.print_('Please, fill the search query') | ||
return self.prompt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason a method is returned here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's in case if user didn't provide information about track. Anyways, I'm gonna change it and use information from the task.
beetsplug/web_tagger.py
Outdated
con.send(data) | ||
if not data: | ||
break | ||
return parse(data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this Thread doesn't have a run
method, which means this code is not actually running in a separate thread. That's fine—this version just blocks the main thread while waiting for a request from the browser—but we should probably just put this code into a plain function instead of a threading.Thread
subclass.
Oh, also, for consistency with other plugins' names, I suggest the name |
Looking great! It also seems that Travis has a few style suggestions, if you're interested in applying those tweaks: https://travis-ci.org/beetbox/beets/jobs/187702547#L848-L854 |
Ok. By the way, what should I do with init.py? Do I need to include it in the PR? |
No, just the main code for the plugin here is fine. |
I left untouched two errors: |
Great! I took care of those two. (Meanwhile, I don't know what's going on with AppVeyor.) The HTTP parser thing looks awesome, but the way. It's very short and easy to read. I think this is close to ready to merge! All we really need now is documentation. |
beetsplug/mbweb.py
Outdated
self.host = '127.0.0.1' | ||
self.port = PORT | ||
try: # Start TCP socket, catch soket.error | ||
self.run_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might suggest the name sock
or something for the socket, for clarity's sake.
beetsplug/mbweb.py
Outdated
return parse_qs(parsed.query)['id'][0] | ||
|
||
|
||
class Server(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably inherit from object
.
beetsplug/mbweb.py
Outdated
.format(urlencode(query)) | ||
webbrowser.open(url) | ||
id_choice = self.server.listen() | ||
search_ids.append(id_choice) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm; looks like this opens the browser for every track on the album? Maybe we just want to search by album?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But what if there are non-album tracks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not sure I understand. Weird cases, like missing or extra tracks, would be handled by the ordinary import flow (exactly the same as if you had entered an ID manually).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I was talking about. Okay then.
beetsplug/mbweb.py
Outdated
from beets.ui.commands import PromptChoice | ||
from beets import ui | ||
|
||
PORT = 8000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move to configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I need to follow PEP8 on docstrings too? I saw a standard library file with a long docstring (>80) ee79e15
Yep! Sadly, the standard library isn't all that PEP8-clean. There are lots of old names |
bb050e2
to
6d4df1b
Compare
7073508
to
1f67052
Compare
Is there still interest in getting this merged? |
Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward? This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Added http server.