browser: read tags from local songs
[ncmpcpp.git] / src / browser.cpp
blob375498bc2eb5135deb10c705618b404e742dae9d
1 /***************************************************************************
2 * Copyright (C) 2008-2012 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include <dirent.h>
22 #include <sys/stat.h>
23 #include <cerrno>
24 #include <cstring>
25 #include <algorithm>
27 #include "browser.h"
28 #include "charset.h"
29 #include "display.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "playlist.h"
33 #include "settings.h"
34 #include "status.h"
35 #include "utility/comparators.h"
36 #ifdef HAVE_TAGLIB_H
37 # include "tag_editor.h"
38 #endif // HAVE_TAGLIB_H
40 using Global::MainHeight;
41 using Global::MainStartY;
42 using Global::myScreen;
43 using Global::RedrawHeader;
45 using MPD::itDirectory;
46 using MPD::itSong;
47 using MPD::itPlaylist;
49 Browser *myBrowser = new Browser;
51 std::set<std::string> Browser::SupportedExtensions;
53 void Browser::Init()
55 static Display::ScreenFormat sf = { this, &Config.song_list_format };
57 w = new Menu<MPD::Item>(0, MainStartY, COLS, MainHeight, Config.columns_in_browser && Config.titles_visibility ? Display::Columns(COLS) : "", Config.main_color, brNone);
58 w->HighlightColor(Config.main_highlight_color);
59 w->CyclicScrolling(Config.use_cyclic_scrolling);
60 w->CenteredCursor(Config.centered_cursor);
61 w->SetSelectPrefix(&Config.selected_item_prefix);
62 w->SetSelectSuffix(&Config.selected_item_suffix);
63 w->SetItemDisplayer(Display::Items);
64 w->SetItemDisplayerUserData(&sf);
65 w->SetGetStringFunction(ItemToString);
67 if (SupportedExtensions.empty())
68 Mpd.GetSupportedExtensions(SupportedExtensions);
70 isInitialized = 1;
73 void Browser::Resize()
75 size_t x_offset, width;
76 GetWindowResizeParams(x_offset, width);
77 w->Resize(width, MainHeight);
78 w->MoveTo(x_offset, MainStartY);
79 w->SetTitle(Config.columns_in_browser && Config.titles_visibility ? Display::Columns(w->GetWidth()) : "");
80 hasToBeResized = 0;
83 void Browser::SwitchTo()
85 using Global::myLockedScreen;
86 using Global::myInactiveScreen;
88 if (myScreen == this)
90 # ifndef WIN32
91 myBrowser->ChangeBrowseMode();
92 # endif // !WIN32
95 if (!isInitialized)
96 Init();
98 if (myLockedScreen)
99 UpdateInactiveScreen(this);
101 if (hasToBeResized || myLockedScreen)
102 Resize();
104 if (isLocal() && Config.browser_sort_mode == smMTime) // local browser doesn't support sorting by mtime
105 Config.browser_sort_mode = smName;
107 w->Empty() ? myBrowser->GetDirectory(itsBrowsedDir) : myBrowser->UpdateItemList();
109 if (myScreen != this && myScreen->isTabbable())
110 Global::myPrevScreen = myScreen;
111 myScreen = this;
112 RedrawHeader = true;
115 std::basic_string<my_char_t> Browser::Title()
117 std::basic_string<my_char_t> result = U("Browse: ");
118 result += Scroller(TO_WSTRING(itsBrowsedDir), itsScrollBeginning, COLS-result.length()-(Config.new_design ? 2 : Global::VolumeState.length()));
119 return result;
122 void Browser::EnterPressed()
124 if (w->Empty())
125 return;
127 const MPD::Item &item = w->Current();
128 switch (item.type)
130 case itDirectory:
132 if (isParentDir(w->Choice()))
134 size_t slash = itsBrowsedDir.rfind("/");
135 if (slash != std::string::npos)
136 GetDirectory(itsBrowsedDir.substr(0, slash), itsBrowsedDir);
137 else
138 GetDirectory("/", itsBrowsedDir);
140 else
141 GetDirectory(item.name, itsBrowsedDir);
142 RedrawHeader = true;
143 break;
145 case itSong:
147 bool res = myPlaylist->Add(*item.song, w->isBold(), 1);
148 w->Bold(w->Choice(), res);
149 break;
151 case itPlaylist:
153 if (Mpd.LoadPlaylist(locale_to_utf_cpy(item.name)))
155 ShowMessage("Playlist \"%s\" loaded", item.name.c_str());
156 myPlaylist->PlayNewlyAddedSongs();
162 void Browser::SpacePressed()
164 if (w->Empty())
165 return;
167 if (Config.space_selects && w->Choice() >= (itsBrowsedDir != "/" ? 1 : 0))
169 w->Select(w->Choice(), !w->isSelected());
170 w->Scroll(wDown);
171 return;
174 if (isParentDir(w->Choice()))
175 return;
177 const MPD::Item &item = w->Current();
178 switch (item.type)
180 case itDirectory:
182 bool result;
183 # ifndef WIN32
184 if (isLocal())
186 MPD::SongList list;
187 MPD::ItemList items;
188 ShowMessage("Scanning directory \"%s\"...", item.name.c_str());
189 myBrowser->GetLocalDirectory(items, item.name, 1);
190 list.reserve(items.size());
191 for (MPD::ItemList::const_iterator it = items.begin(); it != items.end(); ++it)
192 list.push_back(*it->song);
193 result = myPlaylist->Add(list, 0);
195 else
196 # endif // !WIN32
197 result = Mpd.Add(locale_to_utf_cpy(item.name));
198 if (result)
199 ShowMessage("Directory \"%s\" added", item.name.c_str());
200 break;
202 case itSong:
204 bool res = myPlaylist->Add(*item.song, w->isBold(), 0);
205 w->Bold(w->Choice(), res);
206 break;
208 case itPlaylist:
210 if (Mpd.LoadPlaylist(locale_to_utf_cpy(item.name)))
211 ShowMessage("Playlist \"%s\" loaded", item.name.c_str());
212 break;
215 w->Scroll(wDown);
218 void Browser::MouseButtonPressed(MEVENT me)
220 if (w->Empty() || !w->hasCoords(me.x, me.y) || size_t(me.y) >= w->Size())
221 return;
222 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
224 w->Goto(me.y);
225 switch (w->Current().type)
227 case itDirectory:
228 if (me.bstate & BUTTON1_PRESSED)
230 GetDirectory(w->Current().name);
231 RedrawHeader = true;
233 else
235 size_t pos = w->Choice();
236 SpacePressed();
237 if (pos < w->Size()-1)
238 w->Scroll(wUp);
240 break;
241 case itPlaylist:
242 case itSong:
243 if (me.bstate & BUTTON1_PRESSED)
245 size_t pos = w->Choice();
246 SpacePressed();
247 if (pos < w->Size()-1)
248 w->Scroll(wUp);
250 else
251 EnterPressed();
252 break;
255 else
256 Screen< Menu<MPD::Item> >::MouseButtonPressed(me);
259 MPD::Song *Browser::CurrentSong()
261 return !w->Empty() && w->Current().type == itSong ? w->Current().song.get() : 0;
264 void Browser::ReverseSelection()
266 w->ReverseSelection(itsBrowsedDir == "/" ? 0 : 1);
269 void Browser::GetSelectedSongs(MPD::SongList &v)
271 if (w->Empty())
272 return;
273 std::vector<size_t> selected;
274 w->GetSelected(selected);
275 if (selected.empty())
276 selected.push_back(w->Choice());
277 for (auto it = selected.begin(); it != selected.end(); ++it)
279 const MPD::Item &item = w->at(*it);
280 switch (item.type)
282 case itDirectory:
284 # ifndef WIN32
285 if (isLocal())
287 MPD::ItemList list;
288 GetLocalDirectory(list, item.name, 1);
289 for (auto j = list.begin(); j != list.end(); ++j)
290 v.push_back(*j->song);
292 else
293 # endif // !WIN32
295 Mpd.GetDirectoryRecursive(locale_to_utf_cpy(item.name), [&v](MPD::Song &&s) {
296 v.push_back(s);
299 break;
301 case itSong:
303 v.push_back(*item.song);
304 break;
306 case itPlaylist:
308 Mpd.GetPlaylistContent(locale_to_utf_cpy(item.name), [&v](MPD::Song &&s) {
309 v.push_back(s);
311 break;
317 void Browser::ApplyFilter(const std::string &s)
319 w->ApplyFilter(s, itsBrowsedDir == "/" ? 0 : 1, REG_ICASE | Config.regex_type);
322 bool Browser::hasSupportedExtension(const std::string &file)
324 size_t last_dot = file.rfind(".");
325 if (last_dot > file.length())
326 return false;
328 std::string ext = file.substr(last_dot+1);
329 lowercase(ext);
330 return SupportedExtensions.find(ext) != SupportedExtensions.end();
333 void Browser::LocateSong(const MPD::Song &s)
335 if (s.getDirectory().empty())
336 return;
338 itsBrowseLocally = !s.isFromDatabase();
340 if (myScreen != this)
341 SwitchTo();
343 if (itsBrowsedDir != s.getDirectory())
344 GetDirectory(s.getDirectory());
345 for (size_t i = 0; i < w->Size(); ++i)
347 if ((*w)[i].type == itSong && s.getHash() == (*w)[i].song->getHash())
349 w->Highlight(i);
350 break;
355 void Browser::GetDirectory(std::string dir, std::string subdir)
357 if (dir.empty())
358 dir = "/";
360 int highlightme = -1;
361 itsScrollBeginning = 0;
362 if (itsBrowsedDir != dir)
363 w->Reset();
364 itsBrowsedDir = dir;
366 locale_to_utf(dir);
368 w->Clear();
370 if (dir != "/")
372 MPD::Item parent;
373 parent.name = "..";
374 parent.type = itDirectory;
375 w->AddOption(parent);
378 MPD::ItemList list;
379 # ifndef WIN32
380 if (isLocal())
381 GetLocalDirectory(list);
382 else
384 Mpd.GetDirectory(dir, [&list](MPD::Item &&i) {
385 list.push_back(i);
388 # else
389 Mpd.GetDirectory(dir, [&list](MPD::Item &&i) {
390 list.push_back(i);
392 # endif // !WIN32
393 if (!isLocal()) // local directory is already sorted
394 std::sort(list.begin(), list.end(), CaseInsensitiveSorting());
396 for (MPD::ItemList::iterator it = list.begin(); it != list.end(); ++it)
398 switch (it->type)
400 case itPlaylist:
402 utf_to_locale(it->name);
403 w->AddOption(*it);
404 break;
406 case itDirectory:
408 utf_to_locale(it->name);
409 if (it->name == subdir)
410 highlightme = w->Size();
411 w->AddOption(*it);
412 break;
414 case itSong:
416 bool bold = 0;
417 for (size_t i = 0; i < myPlaylist->Items->Size(); ++i)
419 if (myPlaylist->Items->at(i).getHash() == it->song->getHash())
421 bold = 1;
422 break;
425 w->AddOption(*it, bold);
426 break;
430 if (highlightme >= 0)
431 w->Highlight(highlightme);
434 #ifndef WIN32
435 void Browser::GetLocalDirectory(MPD::ItemList &v, const std::string &directory, bool recursively) const
437 DIR *dir = opendir((directory.empty() ? itsBrowsedDir : directory).c_str());
439 if (!dir)
440 return;
442 dirent *file;
444 struct stat file_stat;
445 std::string full_path;
447 size_t old_size = v.size();
448 while ((file = readdir(dir)))
450 // omit . and ..
451 if (file->d_name[0] == '.' && (file->d_name[1] == '\0' || (file->d_name[1] == '.' && file->d_name[2] == '\0')))
452 continue;
454 if (!Config.local_browser_show_hidden_files && file->d_name[0] == '.')
455 continue;
456 MPD::Item new_item;
457 full_path = directory.empty() ? itsBrowsedDir : directory;
458 if (itsBrowsedDir != "/")
459 full_path += "/";
460 full_path += file->d_name;
461 stat(full_path.c_str(), &file_stat);
462 if (S_ISDIR(file_stat.st_mode))
464 if (recursively)
466 GetLocalDirectory(v, full_path, 1);
467 old_size = v.size();
469 else
471 new_item.type = itDirectory;
472 new_item.name = full_path;
473 v.push_back(new_item);
476 else if (hasSupportedExtension(file->d_name))
478 new_item.type = itSong;
479 mpd_pair file_pair = { "file", full_path.c_str() };
480 MPD::MutableSong *s = new MPD::MutableSong(mpd_song_begin(&file_pair));
481 new_item.song = std::shared_ptr<MPD::Song>(s);
482 # ifdef HAVE_TAGLIB_H
483 // FIXME
484 if (!recursively)
485 TagEditor::ReadTags(*s);
486 # endif // HAVE_TAGLIB_H
487 v.push_back(new_item);
490 closedir(dir);
491 std::sort(v.begin()+old_size, v.end(), CaseInsensitiveSorting());
494 void Browser::ClearDirectory(const std::string &path) const
496 DIR *dir = opendir(path.c_str());
497 if (!dir)
498 return;
500 dirent *file;
501 struct stat file_stat;
502 std::string full_path;
504 while ((file = readdir(dir)))
506 // omit . and ..
507 if (file->d_name[0] == '.' && (file->d_name[1] == '\0' || (file->d_name[1] == '.' && file->d_name[2] == '\0')))
508 continue;
510 full_path = path;
511 if (*full_path.rbegin() != '/')
512 full_path += '/';
513 full_path += file->d_name;
514 lstat(full_path.c_str(), &file_stat);
515 if (S_ISDIR(file_stat.st_mode))
516 ClearDirectory(full_path);
517 if (remove(full_path.c_str()) == 0)
519 const char msg[] = "Deleting \"%s\"...";
520 ShowMessage(msg, Shorten(TO_WSTRING(full_path), COLS-const_strlen(msg)).c_str());
522 else
524 const char msg[] = "Couldn't remove \"%s\": %s";
525 ShowMessage(msg, Shorten(TO_WSTRING(full_path), COLS-const_strlen(msg)-25).c_str(), strerror(errno));
528 closedir(dir);
531 void Browser::ChangeBrowseMode()
533 if (Mpd.GetHostname()[0] != '/')
535 ShowMessage("For browsing local filesystem connection to MPD via UNIX Socket is required");
536 return;
539 itsBrowseLocally = !itsBrowseLocally;
540 ShowMessage("Browse mode: %s", itsBrowseLocally ? "Local filesystem" : "MPD database");
541 itsBrowsedDir = itsBrowseLocally ? Config.GetHomeDirectory() : "/";
542 if (itsBrowseLocally && *itsBrowsedDir.rbegin() == '/')
543 itsBrowsedDir.resize(itsBrowsedDir.length()-1);
544 w->Reset();
545 GetDirectory(itsBrowsedDir);
546 RedrawHeader = true;
549 bool Browser::DeleteItem(const MPD::Item &item)
551 // parent dir
552 if (item.type == itDirectory && item.name == "..")
553 return false;
555 // playlist created by mpd
556 if (!isLocal() && item.type == itPlaylist && CurrentDir() == "/")
557 return Mpd.DeletePlaylist(locale_to_utf_cpy(item.name));
559 std::string path;
560 if (!isLocal())
561 path = Config.mpd_music_dir;
562 path += item.type == itSong ? item.song->getURI() : item.name;
564 if (item.type == itDirectory)
565 ClearDirectory(path);
567 return remove(path.c_str()) == 0;
569 #endif // !WIN32
571 void Browser::UpdateItemList()
573 for (size_t i = 0; i < w->Size(); ++i)
574 if ((*w)[i].type == itSong)
575 w->Bold(i, myPlaylist->checkForSong(*(*w)[i].song));
576 w->Refresh();
579 std::string Browser::ItemToString(const MPD::Item &item, void *)
581 switch (item.type)
583 case MPD::itDirectory:
585 return "[" + getBasename(item.name) + "]";
587 case MPD::itSong:
589 if (!Config.columns_in_browser)
590 return item.song->toString(Config.song_list_format_dollar_free);
591 else
592 return Playlist::SongInColumnsToString(*item.song, 0);
594 case MPD::itPlaylist:
596 return Config.browser_playlist_prefix.Str() + getBasename(item.name);
598 default:
600 return "";