From 4c065c064c447581619ff68302f494b7fb61bd23 Mon Sep 17 00:00:00 2001 From: "Dr.Best" Date: Sat, 27 Feb 2010 12:52:53 +0000 Subject: [PATCH] Merlin Music Player and iDream - initial checkin --- Makefile.am | 1 + configure.ac | 8 + merlinmusicplayer/CONTROL/control | 10 + merlinmusicplayer/Makefile.am | 1 + merlinmusicplayer/meta/Makefile.am | 5 + merlinmusicplayer/meta/merlinmusicplayer.jpg | Bin 0 -> 58246 bytes .../meta/plugin_merlinmusicplayer.xml | 24 + merlinmusicplayer/src/Makefile.am | 7 + merlinmusicplayer/src/MerlinMusicPlayer.png | Bin 0 -> 3416 bytes merlinmusicplayer/src/__init__.py | 0 merlinmusicplayer/src/iDream.png | Bin 0 -> 3599 bytes merlinmusicplayer/src/images/Makefile.am | 3 + merlinmusicplayer/src/images/dvr.png | Bin 0 -> 2055 bytes merlinmusicplayer/src/images/dvr_pau.png | Bin 0 -> 1126 bytes merlinmusicplayer/src/images/dvr_pl.png | Bin 0 -> 1127 bytes merlinmusicplayer/src/images/dvr_rep.png | Bin 0 -> 1780 bytes merlinmusicplayer/src/images/dvr_shuf.png | Bin 0 -> 1802 bytes merlinmusicplayer/src/images/mmp3p.png | Bin 0 -> 3324 bytes merlinmusicplayer/src/images/mmp3pHD.png | Bin 0 -> 9972 bytes merlinmusicplayer/src/images/mmpborderHD.png | Bin 0 -> 3014 bytes merlinmusicplayer/src/images/no_coverArt.png | Bin 0 -> 10622 bytes merlinmusicplayer/src/images/placeholder1.png | Bin 0 -> 114 bytes merlinmusicplayer/src/images/progressbar.png | Bin 0 -> 1052 bytes merlinmusicplayer/src/merlinmp3player/Makefile.am | 15 + .../src/merlinmp3player/merlinmp3player.cpp | 404 +++ .../src/merlinmp3player/merlinmp3player.h | 105 + merlinmusicplayer/src/plugin.py | 2726 ++++++++++++++++++++ 27 files changed, 3309 insertions(+) create mode 100644 merlinmusicplayer/CONTROL/control create mode 100755 merlinmusicplayer/Makefile.am create mode 100755 merlinmusicplayer/meta/Makefile.am create mode 100644 merlinmusicplayer/meta/merlinmusicplayer.jpg create mode 100755 merlinmusicplayer/meta/plugin_merlinmusicplayer.xml create mode 100755 merlinmusicplayer/src/Makefile.am create mode 100644 merlinmusicplayer/src/MerlinMusicPlayer.png create mode 100644 merlinmusicplayer/src/__init__.py create mode 100644 merlinmusicplayer/src/iDream.png create mode 100755 merlinmusicplayer/src/images/Makefile.am create mode 100644 merlinmusicplayer/src/images/dvr.png create mode 100644 merlinmusicplayer/src/images/dvr_pau.png create mode 100644 merlinmusicplayer/src/images/dvr_pl.png create mode 100644 merlinmusicplayer/src/images/dvr_rep.png create mode 100644 merlinmusicplayer/src/images/dvr_shuf.png create mode 100644 merlinmusicplayer/src/images/mmp3p.png create mode 100644 merlinmusicplayer/src/images/mmp3pHD.png create mode 100644 merlinmusicplayer/src/images/mmpborderHD.png create mode 100644 merlinmusicplayer/src/images/no_coverArt.png create mode 100644 merlinmusicplayer/src/images/placeholder1.png create mode 100644 merlinmusicplayer/src/images/progressbar.png create mode 100644 merlinmusicplayer/src/merlinmp3player/Makefile.am create mode 100644 merlinmusicplayer/src/merlinmp3player/merlinmp3player.cpp create mode 100644 merlinmusicplayer/src/merlinmp3player/merlinmp3player.h create mode 100644 merlinmusicplayer/src/plugin.py diff --git a/Makefile.am b/Makefile.am index 7f2f94b..ace41bd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -25,6 +25,7 @@ SUBDIRS = \ letterbox \ logomanager \ mediadownloader \ + merlinmusicplayer \ meteoitalia \ mosaic \ moviecut \ diff --git a/configure.ac b/configure.ac index 5a60f95..eacec0f 100644 --- a/configure.ac +++ b/configure.ac @@ -14,6 +14,8 @@ if test x"${have_e2_includes}" = "xyes"; then TUXBOX_APPS_DVB TUXBOX_APPS_LIB_PKGCONFIG(ENIGMA2,enigma2) TUXBOX_APPS_LIB_PKGCONFIG(SIGC,sigc++-1.2) + _TUXBOX_APPS_LIB_PKGCONFIG_OPTIONAL(GSTREAMER,gstreamer,HAVE_GSTREAMER) + _TUXBOX_APPS_LIB_PKGCONFIG_OPTIONAL(GSTREAMERPBUTILS,gstreamer-pbutils,HAVE_GSTSTREAMERPBUTILS) AC_DEFINE(HAVE_E2_INCLUDES, 1,[Define if enigm2 includes are available]) CXXFLAGS="$CXXFLAGS -fno-rtti -fno-exceptions" LDFLAGS="$LDFLAGS -pthread $PYTHON_LDFLAGS" @@ -143,6 +145,12 @@ mediadownloader/meta/Makefile mediadownloader/po/Makefile mediadownloader/src/Makefile +merlinmusicplayer/Makefile +merlinmusicplayer/src/Makefile +merlinmusicplayer/src/images/Makefile +merlinmusicplayer/src/merlinmp3player/Makefile + + meteoitalia/Makefile meteoitalia/meta/Makefile meteoitalia/src/Makefile diff --git a/merlinmusicplayer/CONTROL/control b/merlinmusicplayer/CONTROL/control new file mode 100644 index 0000000..6a77bc2 --- /dev/null +++ b/merlinmusicplayer/CONTROL/control @@ -0,0 +1,10 @@ +Package: enigma2-plugin-extensions-merlinmusicplayer +Version: 1.1 +Description: Merlin Music Player and iDream (music management) +Architecture: mipsel +Section: extra +Priority: optional +Maintainer: Dr. Best +Homepage: http://www.dreambox-tools.info +Depends: enigma2(>2.6git20090615), twisted-web, python-mutagen, python-sqlite3 +Source: http://enigma2-plugins.schwerkraft.elitedvb.net/ diff --git a/merlinmusicplayer/Makefile.am b/merlinmusicplayer/Makefile.am new file mode 100755 index 0000000..7a31bf0 --- /dev/null +++ b/merlinmusicplayer/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = meta src diff --git a/merlinmusicplayer/meta/Makefile.am b/merlinmusicplayer/meta/Makefile.am new file mode 100755 index 0000000..c9f8622 --- /dev/null +++ b/merlinmusicplayer/meta/Makefile.am @@ -0,0 +1,5 @@ +installdir = $(datadir)/meta/ + +dist_install_DATA = plugin_merlinmusicplayer.xml + +EXTRA_DIST = merlinmusicplayer.jpg diff --git a/merlinmusicplayer/meta/merlinmusicplayer.jpg b/merlinmusicplayer/meta/merlinmusicplayer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..99371e78bbd0be8bb5a77e9bc9c1565953c85e1f GIT binary patch literal 58246 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 hzbf%v7ZYkzU`gyQrtk0wA{{cGJWIO-> literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/meta/plugin_merlinmusicplayer.xml b/merlinmusicplayer/meta/plugin_merlinmusicplayer.xml new file mode 100755 index 0000000..381744b --- /dev/null +++ b/merlinmusicplayer/meta/plugin_merlinmusicplayer.xml @@ -0,0 +1,24 @@ + + + + + + Dr.Best + Merlin Music Player + enigma2-plugin-extensions-merlinmusicplayer + Merlin Music Player and iDream + Manage your music files in a database, play it with Merlin Music Player + + + + Dr.Best + Partnerbox + enigma2-plugin-extensions-merlinmusicplayer + Merlin Music Player und iDream + Datenbankverwaltung für Musikdateien, Abspielen mit Merlin Music Player + + + + + + diff --git a/merlinmusicplayer/src/Makefile.am b/merlinmusicplayer/src/Makefile.am new file mode 100755 index 0000000..d2813d0 --- /dev/null +++ b/merlinmusicplayer/src/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = images merlinmp3player + +installdir = /usr/lib/enigma2/python/Plugins/Extensions/MerlinMusicPlayer + +install_PYTHON = *.py merlinmp3player/merlinmp3player.so + +install_DATA = *.png diff --git a/merlinmusicplayer/src/MerlinMusicPlayer.png b/merlinmusicplayer/src/MerlinMusicPlayer.png new file mode 100644 index 0000000000000000000000000000000000000000..ad519dc1a6745438525b76252eb57be9ee6eaf2f GIT binary patch literal 3416 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 Z@p7q$r7b?P$1f59fV6W(^x9rY`wtM$l!E{O literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/__init__.py b/merlinmusicplayer/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/merlinmusicplayer/src/iDream.png b/merlinmusicplayer/src/iDream.png new file mode 100644 index 0000000000000000000000000000000000000000..cb0fa32105bde6aa6e4deaca7ca97ab33c04e6f1 GIT binary patch literal 3599 zcmd^>`!^Gg|Hl_|n`;W=EscmY=91gUtue&f5aycOkUN{1OS#P$DY-KtwwX( zW{kVh2LTMSaIgRX?v5JpQC9%~UI5zO*@};kPekmn*kP%|;sP>91*H!7?}VJ98u+x) z2?Y>DL0Vl|SXoI{M^#8iLmI9NKBcCnrJ-u5uMRhY%W9odGCZ#fw=uIgb=E>x79D43 zgVJ=2()Ecv>rb?R@>=VItc|4{&x%@}h1y$6VG#l@)-qlgIXi?Z5&?IzQ+0GW>w!^q zcQNvFkO{&F2f2b{JwY+vGX577aafgLtTDkuB|cC(E=beH#s*=Jws!GAUvO}Dv37NJ zHMjRex&_&LN88~pAw!660XQfBXrCBA4e4? z^k8^elSxLaaaNyQ3eAg>gH0<%l3B>?R#bk6D}{;8;b3nxVhh`YGAn|z82-8ULJI1G zO1be_Oj2n*k=7bt+kU=i(5GhHzj82!K1dEfLWz;PnFOYUs%8^TlH>FXlT=F*_3k7n z)Dx95lThSjxBN&XJ>IfBO`$9k+C&j$+(qZ>< zjatgoTPk%sZlB4?u%)KD7G_x$)jQC@dwG*CaPm&)244 z7-iOs(u?d$+ujOKIxUc09(3pSg~nPJE(6)taIuSxZJ-CT=mc&9?%o~3gZsD+4xz6~ zVX#cAzxMc8qt?4#wQ<_H5w=}_d)UVT=!XHFjVb8vQ|NeqIGIe&Dx_wWmlc&~6crVv zP#eglEOO<8g37jv`jV{MtgL!&4yUJt$tr4WDdn^`Fv-=-q9)D_Ze#lW#{4dBcGrVa zW<9g6silF#qPMhh+vr>_m)bnQXdNh(Furp(3W@cq&Wp8iqU=assYv-o` z0EYm0N0igQ|K9+dwgglTDiMmZ!NvjrV*UT8LsDvX2L%CS@m6klr$`*0=o=FVK>LP7 z;5AWJ&X71=O+8I*)QaKG!6DujVd3nCY%LLYcAA&%i}mzg%+wHnJxnu3Whfc0x*=6C zqxx$N^hJAt+_3$PMF1z88e|R8TNfKu{ZI}R_B|o?k*xM5{KLP2i4W>DjOqoAwg|g z@`?EAxsZ8>sUHd>Z@Y1N`V5oGZsc`ugRzRF+)BZXu*Rn|r5k2HKKhP!k-+-j%XStb z^YUJu(bqecyh1lB<-6!wKg#?iZo%$bw;$Z{{%iGDlm6`8Ov9DUwb0@GXPL$M&+dzU zubUKm=et+hyP0HZ%^%sp>~&477S=nIrnob1uu-(XFGu%0>0~6Y-r)~8?s&-cA`;S(7fjX=4Qn%4u|_zd~APz_X9_gf>Locs(+77;&L~|FHb<$-B+*^Bl?XKgxaT` z|0tPe-CG4ahQr6=p^N8+{o1F^G&gqkl|0p?mr_lY&`ZEn&4>JAD&{TlB6m9ai-xAA zixF04ErtVKYkOhZ)&iFon|EkjUAWiV{;WUv1|NoTOX0Fn9t1Jm^2VWkzsI`@CVE|N zYj?yKsJ0*BY4TMjC!-xKNqG;5hF8KXQG9AL!a(NSJ3KogHnw8@q{$;q%38Yraj)2R zX+E`g%I1F2NiWPamHVU=OXPdSFF}+%Mc`_O()Kd$Ur%QbK#jkEW8gJ-jyow;de+o0 zB2>Rq{{1aXN!;5>q}a%(&k*HxJ8{{(_gdjRl(C%r^=wlNe@eC0uT=jVr=tO)sv^8u zBkyJZvtHr>ot}nWEG`ynaeFcSQeJbqePrOWfGL5kfB_4p9yxvI*QXkDB4yL!%_jE%cMJqq^EgoI99x~9}aQCg?1~34S{W2nwQV2LM1e|$~wUR&>S`1$FS(P@I zf>`oQ(y#GmK@hYt)A|%K^|z0ph*66La~C|kFE^(8v36`?=a8tx)Q~T8uGRxsoIm*Z z7Zh>kYgHzDHv{-k_84(XiQf)!yag>K{{aGB0Hj&gppCn4-P6vR**4So`Y2B3zW_jH z(cL^ML`l%kcTaUOjeO|B4ZW0HCISi^#q$pel0nNr9rBcEit>r9xwWPztf$noAidq| zIbPv$i7zYlcdZ!YV+z0F6xz8_sj|zCCOoe z@+|ocu~ks49fj~l9M0*fFzvDvtSSE;m_j%_=>@fy#zA@qEiqNo0@Lyz1}O>V9_1Vm zM2>|&kI8?r);(l@==KDPcMzYNdMWkO(TdpD5DkM5p;olIz4nnkb=M;?W`>XTs!N+| zF6+$O*`iR=HMCE^$CeYHrXL9xnR_~?$`e z&}P0NZ1Hxk;}g>GjomVTe?bpcCd}hc*XOIB9L%%(MVJ(mp8d{7#z0%d zFMFQ!Jsou3mq9a~3@ppYYA%7p7>o z6M~~vvVV>qWu{0ydF2WT)b+2LOO0`aJm-+kM?WXTJsEp|oT@Z7SW~EI>{5u6-d(xaepzDVRxs%fHv=ridnh%YfOOED;Pw>y)g zaSy+Ee)}v@=T_q~MxW)OI~u1HOZ;dcettq-TI%no>7^Asic2K;-+yG&-2XV}G!*(Q zKu0JlHTz6i)HY&iqUM-F9)&^?c6ob`vCXbWI7)!!Bz^@>j!$Voizh&!I5j=FW*6hP zF(UOJbV8<wBD>+-)l_$4!afHpuOtvz@yc}BM^0M4B?pl?qqk5uT_}R z4dY~+gFa{1pwZVc(sBKKxb^?aKE5-id%^2`?>&aa_{eQlnX zU(`7ky|mkEk^_>b?5fyGl5&5vpqWZ6blq-D@FyPi!vi*;f#UfnVOUCi;ST9JiNrU4 zMburmq)*aH1y(p3!12nNm*;}VGD_(}Puo8Cjf*AbQHMX*E7z~Dtgra)1yiXXRFySq z>jSGB;S%L38uix)wWtYqG8?Yvq25A_bm%59-o$*Dx4o`Y_&D?k(nH{(I>*v!6 zJ{M(&YY7{nsiNo7Qm5bl{2s2LBs#HuQK+l#k8)k87T7XgQB={yyYeBsuA!#}l*#iU z2wM?D*9lQTw?)GEq6>?^y)G&!=#G9A&Cy&XS?IAYB)DA46$jsGDDN~DVi|yM@eyWa z%%|^~0jz7ce#R?q5)`{W0jr}WB7YAzRpD$wXXyLoUr9!!&eZ&ZR$Ha#t(!#!bv55! zzDfFPd|tBYICLrZr>IXtlrX-^!{m;FC~Dp4Cr#wS2F`RBYG!O~47*BU^g5p^csM>) zlNKyx=@=tDFq}UNOU`epd@?O=E@lGUp(W32h1pjHADjP)Xsx|4G+L(ivZmHi!a7Hjwf*(F^%yV=3VPO^)7Bga>fP#b~!6HezNhVPy*520Qo}DH_ zbUnj8QKnHOG*CE@IJc1vND>ljwe~)1K6!b0FDMW+6cpOVe-arIw6wJLX=yA?eT~ce z%B@Cbspmgbe&+T4Kp7cZ#9J|%|Ki%dI>b7T(*KIfie6q`+&4Eah2<}f?>E9Xn3$Lk zB~pdK@im+OIELVV#s1ZbiuQ(vWs0|IyZ`xHTdvuz^HfxqNJt@7naAVDlFt3nSXh3@ z|3Sq;GMW85pa1b)UGqvxM63U^JUsevaP)6)`d~7xLoSb&R|K!k! zFm%oPcXw3BRP|e1FDDM2*PU@FDEOzR>cv0V(6I4oX%ZVN|Ns9=#!6R|*0UZSc*Fjd zve^_<6mY%%Ru>l`MR|pVg<7cbPEJnLzi}^?|KZDgZMger%V_qJk~U9yJ){5mP*5&% zE<3?HaBy%y!$9+Qcl3gS@L^#dUA{4w{#eLZ84(fojg2xB6K>9KF_`|H*#A;MMGZz;)pa12@SnyR>fq{YUMMV`A6=!E>W@cs)5fRL*LvR2900DGTPE!Ct z=GbNc00ierL_t(|+Rc{tTUu8b$L-#GuZ(6k%}&$oz175MjiYK3jm9|Z*0@*Hx~*}M zBF3Vkaj*o5Kon4*PC(l*G{$J){1-j!Nuu2>*utJMzHIZR0&hPOtBa ziptoG+nY0@qV`>1{(rE0&*zgCkBPMoz(D%k+&rSWbU=LTi|iO23#5F^j!`05e3!4? z4u0jpYu$~k8;#d+`&uKk>kzEGJuz*yI2b=MMz(vB%mqk&W$Ryvwx*)-2H|JfY&I7_ zi=|{Ml_(Z)2@4Bz`2di5ts5B`iL_uvLYsiqCX=GEe{j2E3^pdIo6pK;amD82OV63qR z12dze!lzGg`^i(`=;#cBwf%BXLUMMHV8?l}I4(gDbf)HH%L!WtrcHCGHkmBh$OpwG z#nFDzhee{HTzYg*4?K!P{y;|NyehwWQ!bZx<$P-k24)^Up0Z+1J${H{btgzwot>R3 zY4(O-N!e8?M|wdJXX61Fii&0t6&djSuH7fP;LPcAPsQ*h1Po#sO^7foUPnjh5Tx>^G`)R?HneEP(h{(K>R-ZwT>IbpB8eC+ z6l=-`OEW#KftuE2miy$3B}b_m&rGV%(brbQYHsE==-U@zv_wtu9p2VxEjq+Hwtvtj zS}RTBz&j8N*WFXpBVtKe`6s9dr;QjU$n_aQjvyw9&2l_Kq5Xxq|@jzj^XQ@(x11K$IHT^;=xa zD&W2NE^(_aaNKWYWo+4L!P>bcBMa@f@@i_A`mJ4#`>k<2*YQ<|ebepd*+F+&@E^O= lRonkfclznd-Rb{E{{WS2MV)bPJcSzsO{wF*keaVHI28Km98d+jK%ny3Z$dqxx-Oz!7 zw~C#K<-&q;1^O`sp58t8e@iLQ#=tiYZc^S&d4{m*&A_|k<&21b`D%$z$O*y}nAC^WFuiZHWW zSWvFuz`$F@&O|?^;PtnE>dRqz55KRQM)Dq1fqLoYR1FCkv9AV;sJ zOt-2;x1vb5tWdYKKzCN3PFJIDN4;)Ch3<+OI*X?0%;?sc+Nm?4Rp--X&9~<@U!B!_ zep>VC3C+8Qv~C^Hy1q~A>K?62yR^>l&^o?R`{;V@LuO-W{Se$f)@gHu|Q6f=9l^N-yYoWToy2UBI6xSkWhgnQ$f_cf2`NaIPQK8 z=miN~IDCpV@qc2%j|J7QjgOe+w#e!AW#>yOECNctV7PchP4SEL&4_2u*Ec?sbGpXL z(v)7;kju=Wpw^-0m>VbJ%Ts^!iG)G(lf7!@5h4l=GY-r;vZOw7#e=H{gevx%+;kLi zU@&Yn`RMiUc*ucYOwlvy!xUnQXMF>Ys8ZZhwQDx>u9UmpRNLOOxjF0c}tgZdN;yuYcd*z@-US z`)|+Rxct6zkJ|l)#_A?Ug@y*JiiVm-bB2#@^zzr}DM7#a#mi76D;qd5bNQ9n5nS9)TRQ(ZHbj z@fU8r()I~!?2jlgY!m>U>%WANzGk=3ew4>}rx zBrilVFtYUMiv}3|E0d2lkV%>#@b`E<&%C`)95}yy)wXV6NZM%q#Y-$@?UT>WFI-sy z9HzdNck*%KS;vf`?|8s;#3^&)F}`%u3T9 zasp!`;z5j#Xye_5@yA6iHa?L9dN}dG^!0D}{_a*+ZqVcd2LF%G!UnB2mHnDL0tpOP W*iYoda;x}(@~Ee)pUXO@geCw+!S|d1 literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/images/dvr_shuf.png b/merlinmusicplayer/src/images/dvr_shuf.png new file mode 100644 index 0000000000000000000000000000000000000000..570c1a8e4555b1b6b897d7be2c0b711dbcbc57ee GIT binary patch literal 1802 zcmeAS@N?(olHy`uVBq!ia0vp^ra-L3!3-o%yiQC6Qk(@Ik;M!Q+`=Ht$S`Y;1W=GA z-O<;Pfnj4`&F{d;K)yn&DN!#W zUaufWFE>jsBUP`dTDPi1cUGUyq;{R&X5Fqv-Hv+Q)>_@#GTp6Jv@Y$^I=@5f>^7}ao3)N_)IPdid;eEaktaVzOdvI57p1`hcY!3Kt;Bn2je zPmWs;F*q;?F7=K%JD1m0+JRwX!j7Zt!Q$+Pqjx*+-=>hlz|_Hd_GbcD>|=4kHvY2q zdZjIlECQRYoP-yz=~~ESf4RSY`;`EbyT86MH9|joyL2s&g-Nq zcD*WHz{jWX@!%|3Jw5>kp8{W}ta!yU@4K2_+`8A;;eSoWps<0V>Gsh@f5ngeS>7z; z7Tutw7&L!P!Yu|Sj#uk-d0zE!wANSLci6k(=`~$}qeq#a{SB%6$gaaE;9w9e>AGKW zrv8~9@?uV(?gTj`+c_VxkDm8~kINJ2$40iDKg>nmf39oFND6+KRMD^U@#Y^D51>B{ z8}FVyu%h`@tzZZHj5j+kEI!vGtidYa;ILbyrNOVNUu$I*f6ml1)jcJjuDcH9<Hm^7K+i^s zUk(n)N@w0Y)lu_-t(fohhFKCI;p_MF1)>l1u}5is;Ga};Z|@I&f71kC9tOsiCEbsW zERr4H{L(&nK!Nw`-@WSR6Q9MgWG4Y_Y0+!!-hbdrE=Ty>=vlIds?Huz;JjKQx2O~7 z;0DG2!ZV)FE3&g>K6ltI+U~QX&<-``Be7==98u*tr}h5*p(0CQV)&7>vyeGU+U3RC zGxb-Fe4XAXI7jRMK~qBp76I-HC2UhZy_^0)S|m1HLSo1N29vqYD;PNx#LjGbI!DGU z@`uojy$xULRlx2Hl>23{=aIW-nSDbh+YE#AO0$7U;fB}6>=SdQJ6w&Qx&Mx1p^yVZ z;;T=I75`)p3fFS0*v;G1_=>-Q;ZV|a{=>3z60^Swm@EjeLL#{d8T literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/images/mmp3p.png b/merlinmusicplayer/src/images/mmp3p.png new file mode 100644 index 0000000000000000000000000000000000000000..30d271bf7db24b31b261f914b6ad8352da9f8aa5 GIT binary patch literal 3324 zcmeHJX;4#H7QWdaBw+|D5)dJ5B8w9C9bQ6!pgk#xqMIZ zS4n$P5|j2O@BM0T%DzZ6J|ZeEbT_&yEGj-RIWA$3FPF~=2;C7Vh>lA_V-urd5`@w5 zW_B!ddru1owga-$k?H5|9q8c`#N$T>3xvCcv0;%hf{18-II3Y}x6#;6bBn!}iG%i5 zhfSuAI%ZCr&7JAnT=loR8d$m+GTe=6Q9NF8Qyeyr)87@Ay?!I;&JDffJ zomhS@>;R@O58(u{b_9F-@A3%<_2Y%@3=#x{g!>2YrqZp{^{v+#Sg$v<*b?1oxKKNQ>BPnk#W+4{-EMqrk0x~Wp(iR)bSZn~g%%|PV2L+be%QG2x|I=b zO&x|vze6fkLo1gXOh3A8fV=UT$_0I|ThK#pICINe3>G|GzmNL5pgFI_N!P~yCLksUGP2WQX9 zC(W;Z>I(Q*Yq-xoP!4f08e$wo_80Egl+WnA9Fbw`TGrXX@1x(y* zYMq0?)ZFtN_XwP;pX1Ulwz0pQ&@I=X2_NKqE}M7r!_&o2pHNlu=6y$hu~Z7kt|G!M z=VliBkA)+=D4?4FYjR`U?(H{nB-r>al|%#B5`x!Sdm_E-dwSt{FPK4{&TD5mLGWlW zV#Vxe1jh>AJ))~RQ6!G9THfjXKIfy{pj!vhOP2n|El_gX^`1%xuHert%O7&rq@>-S zU}=9Y=kqFPYbtrRhv(ltuQ#EXv~{_@J)A*Rr6P>*<4v*`)b+To@X@l2i;Gvn9CBB+ zrA)9Dr*IC^QaU?n7h9sGdHpH9O9gWmYJ26XFPIxKcxp&dl}!Ql#OJoh^Ff^He#07m zOuLRdh0El@65G7K*S!TLT;FOgomVXw{MC@#dg_QNM~N;*?Wl`6^UAE-OId^wd3z+^v#!(TQvDxe3%lmzIx@t)v!T8{w|Wo}^8Z9YY|tcQ?kqmCsCixcvG` zltL+wn6|baLZc!5N}a6I1hbe@kK4!4d!B{F=)RzK2V#g#~vo%5DzNSsZ;5B`1$xZV}uTJ6A88v$)tM1O6@A<>iGK{ zZ+4CbUEFFmymn}bxFLg~aUOjpH*h=)i*sdsKpu>`wZx;xf-A`7>6b=WB%&B#&+Vs8 zCms}LX|#g_5~Y)nR;bfvp_0(UJy`uWnF^Pv=#+1;w`N>4b@ZXvVrkO>1eT`Ib2Gd} zJ?uq(8ZR1aXCXH{yXrGP3TqV|q4;QpWmF!b4&4VdQa{)*hC~GxSSpfaCOoY4yg16; z(8m@^tu8CANYZ_2>@m(rAJ)tKkH&d6TXoT;ZIU>J5^rSXRx-~=ohceKy8LaMf)9J9 zP0xJrR6?(L&Dh`i@Fexw(;gUNV~NG4x&9;1eP}nWm#|a#{olL}r_m_KAB)yj%9s?o zm}ZFlu-!x*Kyz#Ps_e|fHkbR=3zGw1pKiHij6{e*{^Dg3QJ(mw;K;(N za-Y0NBp!h5yX^hkDpsJDdew9_j`5R(Mx-KsJ?gCsn!<@0Mr$05?c3#+HYdS6JGiW} z{SFf-C?`V$vDveqM#>Po21-*Lo{DhHhHYdCcVr@Y|LaUbqjeg_i3;&6S@*_@+;M6| zv=4V~bNM+fcTqf=P1$d%m>_9jij0PYwZu?e9?wz?02Zn$ubNSOI4$c)4sMXeqsy|4{X}vRk3nrS*70^OA-h8I@cIxQR>Ze*;XEoS0AFNm9 zM9P(iDm}*(y|=4Abws}-T(k3OHY7&xsnh^D{-@^_?KD>?xq8RtSv5gVVGymL8y1Bg z{+DWT(b`l-Nwk#qWx@y+v{FEYKt+1g>bn^PCMpm{=4FznqkW&2tTR)iz$vu+Yfe+9 ptc8}(l5QYw`g#ShZ?e$*zXJr)X>b4l literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/images/mmp3pHD.png b/merlinmusicplayer/src/images/mmp3pHD.png new file mode 100644 index 0000000000000000000000000000000000000000..bc458820f708f37c19a092c0339ed67e322ea5b9 GIT binary patch literal 9972 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 Q8sKlX)+ep1EC@0G2m1!0VgLXD literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/images/placeholder1.png b/merlinmusicplayer/src/images/placeholder1.png new file mode 100644 index 0000000000000000000000000000000000000000..87f9e5602ca18cba4203ff28850f1a1a439b97a7 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^ra-L3$P6Tp{$>^cQY`6?zK#qG8~eHcB(ehe%mF?j zu0VQumF+Dc#aI&L7tG-B>_!@p!|Und7$R{wIYEN;1W-zVf$`1s$5lWH22WQ%mvv4F FO#uEc8Pxy) literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/images/progressbar.png b/merlinmusicplayer/src/images/progressbar.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a9d196b6d18eb903f2f1ffa7b4b08a9e948bbf GIT binary patch literal 1052 zcmXBT3rtg27zgkV>Ks#B%Bw9cEM#sjW^8dAaht}a#iDH(4Py{>z6#1vVTuoevW^ux zc}ye-W1u1v8AFjFC8H@x5Ku0;x^>r1x zkJ`sdN&v`v4}dQKaClMG0OYI#&~gDb)B=!?RPG(z0)T_Wuae`QpO2Wh7TC zL~T~+xr~s(A2tb9i2yJwI@4e(;1(&iqiW{{AWb@DKW7p8i3; zVT6x)SZH`CFi1`tB*jK)sZn|cGg@XGEjNu-n8qqikE%?MsuA;(8uOF$=JAW>@jCO< zddsuRmgiS|8Z9rH7ABe(US3;x*|IR%ih0#$ecf)Ix@n!dWu5L?oax5M{$7;zF3S6C z3ZYFYvMKv9DzQy9Xjk92s~^}kBX*6%F(>u;*P$D8=pH-u<4*lEr{RUqgwr_bGQD=0 zr(Nb5mqm`a7nE-6tlO$~FKXR3-I86u1HlmM~#WG{2e$L@unARLC}NAc#>SV8?NoZL!?K{{S;89w`6- literal 0 HcmV?d00001 diff --git a/merlinmusicplayer/src/merlinmp3player/Makefile.am b/merlinmusicplayer/src/merlinmp3player/Makefile.am new file mode 100644 index 0000000..7da3d32 --- /dev/null +++ b/merlinmusicplayer/src/merlinmp3player/Makefile.am @@ -0,0 +1,15 @@ +OBJS = merlinmp3player.cpp + +-include $(OBJS:.cpp=.d) + +merlinmp3player.so: merlinmp3player.h + + + $(CXX) $(CPPFLAGS) -MD $(CXXFLAGS) $(ENIGMA2_CFLAGS) $(SIGC_CFLAGS) $(GSTREAMER_CFLAGS) $(GSTREAMERPBUTILS_CFLAGS) $(GSTREAMER_LDFLAGS) $(GSTREAMERPBUTILS_LDFLAGS) \ + $(PYTHON_CPPFLAGS) $(DEFS) -I$(top_srcdir)/include -Wall -lgstbase-0.10 -W $(OBJS) -shared -fPIC -Wl,-soname,merlinmp3player.so -o merlinmp3player.so \ + $(LDFLAGS) + +all: merlinmp3player.so + +CLEANFILES = merlinmp3player.so merlinmp3player.d + diff --git a/merlinmusicplayer/src/merlinmp3player/merlinmp3player.cpp b/merlinmusicplayer/src/merlinmp3player/merlinmp3player.cpp new file mode 100644 index 0000000..0f25e41 --- /dev/null +++ b/merlinmusicplayer/src/merlinmp3player/merlinmp3player.cpp @@ -0,0 +1,404 @@ +/* + MerlinMP3Player E2 + + (c) 2010 by Dr. Best + Support: www.dreambox-tools.info + +*/ + +#include +#include +#include +#include +#include "merlinmp3player.h" +#include +#include +#include +#include + +// eServiceFactoryMerlinMP3Player + +eServiceFactoryMerlinMP3Player::eServiceFactoryMerlinMP3Player() +{ + ePtr sc; + + eServiceCenter::getPrivInstance(sc); + if (sc) + { + std::list extensions; + extensions.push_back("mp3"); + sc->addServiceFactory(eServiceFactoryMerlinMP3Player::id, this, extensions); + } + m_service_info = new eStaticServiceMP3Info(); + +} + +eServiceFactoryMerlinMP3Player::~eServiceFactoryMerlinMP3Player() +{ + ePtr sc; + + eServiceCenter::getPrivInstance(sc); + if (sc) + sc->removeServiceFactory(eServiceFactoryMerlinMP3Player::id); +} + +DEFINE_REF(eServiceFactoryMerlinMP3Player) + + // iServiceHandler +RESULT eServiceFactoryMerlinMP3Player::play(const eServiceReference &ref, ePtr &ptr) +{ + // check resources... + ptr = new eServiceMerlinMP3Player(ref); + return 0; +} + +RESULT eServiceFactoryMerlinMP3Player::record(const eServiceReference &ref, ePtr &ptr) +{ + ptr=0; + return -1; +} + +RESULT eServiceFactoryMerlinMP3Player::list(const eServiceReference &, ePtr &ptr) +{ + ptr=0; + return -1; +} + +RESULT eServiceFactoryMerlinMP3Player::info(const eServiceReference &ref, ePtr &ptr) +{ + ptr = m_service_info; + return 0; +} + +RESULT eServiceFactoryMerlinMP3Player::offlineOperations(const eServiceReference &, ePtr &ptr) +{ + ptr = 0; + return -1; +} + +DEFINE_REF(eStaticServiceMP3Info) + +eStaticServiceMP3Info::eStaticServiceMP3Info() +{ + // nothing to to here... +} + +RESULT eStaticServiceMP3Info::getName(const eServiceReference &ref, std::string &name) +{ + size_t last = ref.path.rfind('/'); + if (last != std::string::npos) + name = ref.path.substr(last+1); + else + name = ref.path; + return 0; +} + +int eStaticServiceMP3Info::getLength(const eServiceReference &ref) +{ + return -1; +} + +// eServiceMerlinMP3Player + +eServiceMerlinMP3Player::eServiceMerlinMP3Player(eServiceReference ref): m_ref(ref), m_pump(eApp, 1) +{ + m_filename = m_ref.path.c_str(); + CONNECT(m_pump.recv_msg, eServiceMerlinMP3Player::gstPoll); + m_state = stIdle; + eDebug("eServiceMerlinMP3Player construct!"); + + GstElement *sink; + GstElement *source; + GstElement *decoder; + + m_gst_pipeline = gst_pipeline_new ("audio-player"); + if (!m_gst_pipeline) + eWarning("failed to create pipeline"); + + source = gst_element_factory_make ("filesrc", "file reader"); + decoder = gst_element_factory_make ("mad", "MP3 decoder"); + sink = gst_element_factory_make ("alsasink", "ALSA output"); + if (m_gst_pipeline && source && decoder && sink) + { + g_object_set (G_OBJECT (source), "location", m_filename.c_str(), NULL); + gst_bin_add_many (GST_BIN (m_gst_pipeline), source, decoder, sink, NULL); + gst_element_link_many (source, decoder, sink, NULL); + gst_bus_set_sync_handler(gst_pipeline_get_bus (GST_PIPELINE (m_gst_pipeline)), gstBusSyncHandler, this); + gst_element_set_state (m_gst_pipeline, GST_STATE_PLAYING); + } + else + { + if (m_gst_pipeline) + gst_object_unref(GST_OBJECT(m_gst_pipeline)); + if (source) + gst_object_unref(GST_OBJECT(source)); + if (decoder) + gst_object_unref(GST_OBJECT(decoder)); + if (sink) + gst_object_unref(GST_OBJECT(sink)); + eDebug("no playing...!"); + } + eDebug("eServiceMerlinMP3Player::using gstreamer with location=%s", m_filename.c_str()); +} + +eServiceMerlinMP3Player::~eServiceMerlinMP3Player() +{ + if (m_state == stRunning) + stop(); + + if (m_gst_pipeline) + { + gst_object_unref (GST_OBJECT (m_gst_pipeline)); + eDebug("eServiceMerlinMP3Player destruct!"); + } +} + +DEFINE_REF(eServiceMerlinMP3Player); + +RESULT eServiceMerlinMP3Player::connectEvent(const Slot2 &event, ePtr &connection) +{ + connection = new eConnection((iPlayableService*)this, m_event.connect(event)); + return 0; +} + +RESULT eServiceMerlinMP3Player::start() +{ + assert(m_state == stIdle); + + m_state = stRunning; + if (m_gst_pipeline) + { + eDebug("eServiceMerlinMP3Player::starting pipeline"); + gst_element_set_state (m_gst_pipeline, GST_STATE_PLAYING); + } + m_event(this, evStart); + return 0; +} + +RESULT eServiceMerlinMP3Player::stop() +{ + assert(m_state != stIdle); + if (m_state == stStopped) + return -1; + eDebug("eServiceMerlinMP3Player::stop %s", m_filename.c_str()); + gst_element_set_state(m_gst_pipeline, GST_STATE_NULL); + m_state = stStopped; + return 0; +} + +RESULT eServiceMerlinMP3Player::setTarget(int target) +{ + return -1; +} + +RESULT eServiceMerlinMP3Player::pause(ePtr &ptr) +{ + ptr=this; + return 0; +} + +RESULT eServiceMerlinMP3Player::setSlowMotion(int ratio) +{ + return -1; +} + +RESULT eServiceMerlinMP3Player::setFastForward(int ratio) +{ + return -1; +} + + // iPausableService +RESULT eServiceMerlinMP3Player::pause() +{ + if (!m_gst_pipeline) + return -1; + gst_element_set_state(m_gst_pipeline, GST_STATE_PAUSED); + return 0; +} + +RESULT eServiceMerlinMP3Player::unpause() +{ + if (!m_gst_pipeline) + return -1; + gst_element_set_state(m_gst_pipeline, GST_STATE_PLAYING); + return 0; +} + + /* iSeekableService */ +RESULT eServiceMerlinMP3Player::seek(ePtr &ptr) +{ + ptr = this; + return 0; +} + +RESULT eServiceMerlinMP3Player::getLength(pts_t &pts) +{ + if (!m_gst_pipeline) + return -1; + if (m_state != stRunning) + return -1; + + GstFormat fmt = GST_FORMAT_TIME; + gint64 len; + + if (!gst_element_query_duration(m_gst_pipeline, &fmt, &len)) + return -1; + + /* len is in nanoseconds. we have 90 000 pts per second. */ + + pts = len / 11111; + return 0; +} + +RESULT eServiceMerlinMP3Player::seekTo(pts_t to) +{ + if (!m_gst_pipeline) + return -1; + + /* convert pts to nanoseconds */ + gint64 time_nanoseconds = to * 11111LL; + if (!gst_element_seek (m_gst_pipeline, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + GST_SEEK_TYPE_SET, time_nanoseconds, + GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) + { + eDebug("eServiceMerlinMP3Player::SEEK failed"); + return -1; + } + return 0; +} + +RESULT eServiceMerlinMP3Player::seekRelative(int direction, pts_t to) +{ + if (!m_gst_pipeline) + return -1; + + pause(); + + pts_t ppos; + getPlayPosition(ppos); + ppos += to * direction; + if (ppos < 0) + ppos = 0; + seekTo(ppos); + + unpause(); + + return 0; +} + +RESULT eServiceMerlinMP3Player::getPlayPosition(pts_t &pts) +{ + if (!m_gst_pipeline) + return -1; + if (m_state != stRunning) + return -1; + + GstFormat fmt = GST_FORMAT_TIME; + gint64 len; + + if (!gst_element_query_position(m_gst_pipeline, &fmt, &len)) + return -1; + + /* len is in nanoseconds. we have 90 000 pts per second. */ + pts = len / 11111; + return 0; +} + +RESULT eServiceMerlinMP3Player::setTrickmode(int trick) +{ + /* trickmode currently doesn't make any sense for us. */ + return -1; +} + +RESULT eServiceMerlinMP3Player::isCurrentlySeekable() +{ + return 1; +} + +RESULT eServiceMerlinMP3Player::info(ePtr&i) +{ + i = this; + return 0; +} + +RESULT eServiceMerlinMP3Player::getName(std::string &name) +{ + name = m_filename; + size_t n = name.rfind('/'); + if (n != std::string::npos) + name = name.substr(n + 1); + return 0; +} + +int eServiceMerlinMP3Player::getInfo(int w) +{ + return resNA; +} + +std::string eServiceMerlinMP3Player::getInfoString(int w) +{ + return ""; +} + +void eServiceMerlinMP3Player::gstBusCall(GstBus *bus, GstMessage *msg) +{ + switch (GST_MESSAGE_TYPE (msg)) + { + case GST_MESSAGE_EOS: + m_event((iPlayableService*)this, evEOF); + break; + case GST_MESSAGE_STATE_CHANGED: + { + if(GST_MESSAGE_SRC(msg) != GST_OBJECT(m_gst_pipeline)) + break; + GstState old_state, new_state; + gst_message_parse_state_changed(msg, &old_state, &new_state, NULL); + if(old_state == new_state) + break; + eDebug("eServiceMerlinMP3Player::state transition %s -> %s", gst_element_state_get_name(old_state), gst_element_state_get_name(new_state)); + break; + } + case GST_MESSAGE_ERROR: + { + gchar *debug; + GError *err; + gst_message_parse_error (msg, &err, &debug); + g_free (debug); + eWarning("Gstreamer error: %s", err->message); + g_error_free(err); + break; + } + default: + break; + } +} + +GstBusSyncReply eServiceMerlinMP3Player::gstBusSyncHandler(GstBus *bus, GstMessage *message, gpointer user_data) +{ + eServiceMerlinMP3Player *_this = (eServiceMerlinMP3Player*)user_data; + _this->m_pump.send(1); + /* wake */ + return GST_BUS_PASS; +} + +void eServiceMerlinMP3Player::gstPoll(const int&) +{ + usleep(1); + GstBus *bus = gst_pipeline_get_bus (GST_PIPELINE (m_gst_pipeline)); + GstMessage *message; + while ((message = gst_bus_pop (bus))) + { + gstBusCall(bus, message); + gst_message_unref (message); + } +} + + +eAutoInitPtr init_eServiceFactoryMerlinMP3Player(eAutoInitNumbers::service+1, "eServiceFactoryMerlinMP3Player"); + +PyMODINIT_FUNC +initmerlinmp3player(void) +{ + Py_InitModule("merlinmp3player", NULL); +} + diff --git a/merlinmusicplayer/src/merlinmp3player/merlinmp3player.h b/merlinmusicplayer/src/merlinmp3player/merlinmp3player.h new file mode 100644 index 0000000..693cbbb --- /dev/null +++ b/merlinmusicplayer/src/merlinmp3player/merlinmp3player.h @@ -0,0 +1,105 @@ +#include +#include +#include + +class eStaticServiceMP3Info; + +class eServiceFactoryMerlinMP3Player: public iServiceHandler +{ +DECLARE_REF(eServiceFactoryMerlinMP3Player); +public: + eServiceFactoryMerlinMP3Player(); + virtual ~eServiceFactoryMerlinMP3Player(); + enum { id = 0x1014 }; + + // iServiceHandler + RESULT play(const eServiceReference &, ePtr &ptr); + RESULT record(const eServiceReference &, ePtr &ptr); + RESULT list(const eServiceReference &, ePtr &ptr); + RESULT info(const eServiceReference &, ePtr &ptr); + RESULT offlineOperations(const eServiceReference &, ePtr &ptr); +private: + ePtr m_service_info; +}; + +class eStaticServiceMP3Info: public iStaticServiceInformation +{ + DECLARE_REF(eStaticServiceMP3Info); + friend class eServiceFactoryMerlinMP3Player; + eStaticServiceMP3Info(); +public: + RESULT getName(const eServiceReference &ref, std::string &name); + int getLength(const eServiceReference &ref); +}; + +typedef struct _GstElement GstElement; + +class eServiceMerlinMP3Player: public iPlayableService, public iPauseableService, + public iServiceInformation, public iSeekableService, public Object +{ +DECLARE_REF(eServiceMerlinMP3Player); +public: + virtual ~eServiceMerlinMP3Player(); + + // iPlayableService + RESULT connectEvent(const Slot2 &event, ePtr &connection); + RESULT start(); + RESULT stop(); + RESULT setTarget(int target); + + RESULT pause(ePtr &ptr); + RESULT setSlowMotion(int ratio); + RESULT setFastForward(int ratio); + + RESULT seek(ePtr &ptr); + // not implemented + RESULT audioChannel(ePtr &ptr) { ptr = 0; return 0; }; + RESULT audioTracks(ePtr &ptr) { ptr = 0; return 0; }; + RESULT frontendInfo(ePtr &ptr) { ptr = 0; return -1; }; + RESULT subServices(ePtr &ptr) { ptr = 0; return -1; }; + RESULT timeshift(ePtr &ptr) { ptr = 0; return -1; }; + RESULT cueSheet(ePtr &ptr) { ptr = 0; return -1; }; + RESULT subtitle(ePtr &ptr) { ptr = 0; return -1; }; + RESULT audioDelay(ePtr &ptr) { ptr = 0; return -1; }; + RESULT rdsDecoder(ePtr &ptr) { ptr = 0; return -1; }; + RESULT stream(ePtr &ptr) { ptr = 0; return -1; }; + RESULT streamed(ePtr &ptr) { ptr = 0; return -1; }; + RESULT keys(ePtr &ptr) { ptr = 0; return -1; }; + + // iPausableService + RESULT pause(); + RESULT unpause(); + + RESULT info(ePtr&); + + // iSeekableService + RESULT getLength(pts_t &SWIG_OUTPUT); + RESULT seekTo(pts_t to); + RESULT seekRelative(int direction, pts_t to); + RESULT getPlayPosition(pts_t &SWIG_OUTPUT); + RESULT setTrickmode(int trick); + RESULT isCurrentlySeekable(); + + // iServiceInformation + RESULT getName(std::string &name); + int getInfo(int w); + std::string getInfoString(int w); +private: + friend class eServiceFactoryMerlinMP3Player; + eServiceReference m_ref; + std::string m_filename; + eServiceMerlinMP3Player(eServiceReference ref); + Signal2 m_event; + enum + { + stIdle, stRunning, stStopped, + }; + int m_state; + GstElement *m_gst_pipeline; + eFixedMessagePump m_pump; + + void gstBusCall(GstBus *bus, GstMessage *msg); + static GstBusSyncReply gstBusSyncHandler(GstBus *bus, GstMessage *message, gpointer user_data); + void gstPoll(const int&); +}; + diff --git a/merlinmusicplayer/src/plugin.py b/merlinmusicplayer/src/plugin.py new file mode 100644 index 0000000..387b9aa --- /dev/null +++ b/merlinmusicplayer/src/plugin.py @@ -0,0 +1,2726 @@ +# +# Merlin Music Player E2 +# +# +# Coded by Dr.Best (c) 2010 +# Support: www.dreambox-tools.info +# +# This plugin is licensed under the Creative Commons +# Attribution-NonCommercial-ShareAlike 3.0 Unported +# License. To view a copy of this license, visit +# http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter to Creative +# Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA. +# +# Alternatively, this plugin may be distributed and executed on hardware which +# is licensed by Dream Multimedia GmbH. + +# This plugin is NOT free software. It is open source, you are allowed to +# modify it (if you keep the license), but it may not be commercially +# distributed other than under the conditions noted above. +# + +from Plugins.Plugin import PluginDescriptor +from Screens.Screen import Screen +from Components.ActionMap import ActionMap, NumberActionMap +from Components.Label import Label +from enigma import RT_VALIGN_CENTER, RT_HALIGN_LEFT, RT_HALIGN_RIGHT, RT_HALIGN_CENTER, gFont, eListbox,ePoint, eListboxPythonMultiContent + +# merlin mp3 player +import merlinmp3player +ENIGMA_MERLINPLAYER_ID = 0x1014 + +from Components.FileList import FileList +from enigma import eServiceReference, eTimer +from os import path as os_path, mkdir as os_mkdir, listdir as os_listdir, walk as os_walk +from Components.ProgressBar import ProgressBar +from twisted.internet import reactor, defer +from twisted.web import client +from twisted.web.client import HTTPClientFactory, downloadPage +from enigma import getDesktop +from Screens.MessageBox import MessageBox +from Components.GUIComponent import GUIComponent +from enigma import ePicLoad +from xml.etree.cElementTree import fromstring as cet_fromstring +from urllib import quote +from Components.ScrollLabel import ScrollLabel +from Components.AVSwitch import AVSwitch +from Tools.Directories import fileExists, resolveFilename, SCOPE_CURRENT_SKIN +from Tools.LoadPixmap import LoadPixmap +from Components.Pixmap import Pixmap, MultiPixmap +from Components.ServicePosition import ServicePositionGauge +from Screens.InfoBarGenerics import InfoBarSeek, InfoBarNotifications +from Components.ServiceEventTracker import ServiceEventTracker, InfoBarBase +from enigma import iPlayableService, iServiceInformation +from Components.Sources.StaticText import StaticText +from Screens.ChoiceBox import ChoiceBox +from Screens.VirtualKeyBoard import VirtualKeyBoard +from Tools.BoundFunction import boundFunction +from sqlite3 import dbapi2 as sqlite +from mutagen.flac import FLAC +from mutagen.mp3 import MP3 +from mutagen.id3 import ID3 +from mutagen.easyid3 import EasyID3 +from mutagen.easymp4 import EasyMP4 +from mutagen.oggvorbis import OggVorbis +import datetime +from random import shuffle, randrange +from Components.config import config, ConfigSubsection, ConfigDirectory, ConfigYesNo, ConfigInteger, getConfigListEntry, configfile +from Components.ConfigList import ConfigListScreen +from Tools.HardwareInfo import HardwareInfo + + +config.plugins.merlinmusicplayer = ConfigSubsection() +config.plugins.merlinmusicplayer.hardwaredecoder = ConfigYesNo(default = True) +config.plugins.merlinmusicplayer.startlastsonglist = ConfigYesNo(default = True) +config.plugins.merlinmusicplayer.lastsonglistindex = ConfigInteger(-1) +config.plugins.merlinmusicplayer.databasepath = ConfigDirectory(default = "/hdd/") +config.plugins.merlinmusicplayer.usegoogleimage = ConfigYesNo(default = True) +config.plugins.merlinmusicplayer.googleimagepath = ConfigDirectory(default = "/hdd/") +config.plugins.merlinmusicplayer.usescreensaver = ConfigYesNo(default = True) +config.plugins.merlinmusicplayer.screensaverwait = ConfigInteger(1,limits = (1, 60)) +config.plugins.merlinmusicplayer.idreamextendedpluginlist = ConfigYesNo(default = True) +config.plugins.merlinmusicplayer.merlinmusicplayerextendedpluginlist = ConfigYesNo(default = True) +config.plugins.merlinmusicplayer.defaultfilebrowserpath = ConfigDirectory(default = "/hdd/") +config.plugins.merlinmusicplayer.rememberlastfilebrowserpath = ConfigYesNo(default = True) + +from enigma import ePythonMessagePump +from threading import Thread, Lock + +class ThreadQueue: + def __init__(self): + self.__list = [ ] + self.__lock = Lock() + + def push(self, val): + lock = self.__lock + lock.acquire() + self.__list.append(val) + lock.release() + + def pop(self): + lock = self.__lock + lock.acquire() + ret = self.__list.pop() + lock.release() + return ret + +THREAD_WORKING = 1 +THREAD_FINISHED = 2 + +class PathToDatabase(Thread): + def __init__(self): + Thread.__init__(self) + self.__running = False + self.__cancel = False + self.__path = None + self.__messages = ThreadQueue() + self.__messagePump = ePythonMessagePump() + + def __getMessagePump(self): + return self.__messagePump + + def __getMessageQueue(self): + return self.__messages + + def __getRunning(self): + return self.__running + + def Cancel(self): + self.__cancel = True + + MessagePump = property(__getMessagePump) + Message = property(__getMessageQueue) + isRunning = property(__getRunning) + + def Start(self, path): + if not self.__running: + self.__path = path + self.start() + + def run(self): + mp = self.__messagePump + self.__running = True + self.__cancel = False + if self.__path: + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + counter = 0 + for root, subFolders, files in os_walk(self.__path): + if self.__cancel: + break + for filename in files: + if self.__cancel: + break + audio, isAudio, title, genre,artist,album,tracknr,track,date,length,bitrate = getID3Tags(root,filename) + if audio: + # 1. Artist + artistID = -1 + cursor.execute("""SELECT artist_id FROM Artists WHERE artist = "%s";""" % (artist)) + row = cursor.fetchone() + if row is None: + cursor.execute("""INSERT INTO Artists (artist) VALUES("%s");""" % (artist)) + artistID = cursor.lastrowid + else: + artistID = row[0] + # 2. Album + albumID = -1 + cursor.execute("""SELECT album_id FROM Album WHERE album_text = "%s";""" % (album)) + row = cursor.fetchone() + if row is None: + cursor.execute("""INSERT INTO Album (album_text) VALUES("%s");""" % (album)) + albumID = cursor.lastrowid + else: + albumID = row[0] + + # 3. Genre + genreID = -1 + cursor.execute("""SELECT genre_id FROM Genre WHERE genre_text = "%s";""" % (genre)) + row = cursor.fetchone() + if row is None: + cursor.execute("""INSERT INTO Genre (genre_text) VALUES("%s");""" % (genre)) + genreID = cursor.lastrowid + else: + genreID = row[0] + + # 4. Songs + try: + cursor.execute("INSERT INTO Songs (filename,title,artist_id,album_id,genre_id,tracknumber, bitrate, length, track, date) VALUES(?,?,?,?,?,?,?,?,?,?);" , (os_path.join(root,filename),title,artistID,albumID,genreID, tracknr, bitrate, length, track, date)) + self.__messages.push((THREAD_WORKING, _("%s\n added to database") % os_path.join(root,filename))) + mp.send(0) + counter +=1 + except sqlite.IntegrityError: + self.__messages.push((THREAD_WORKING, _("%s\n already exists in database!") % os_path.join(root,filename))) + mp.send(0) + audio = None + if not self.__cancel: + connection.commit() + cursor.close() + connection.close() + if self.__cancel: + self.__messages.push((THREAD_FINISHED, _("Process aborted.\n 0 files added to database!\nPress OK to close.") )) + else: + self.__messages.push((THREAD_FINISHED, _("%d files added to database!\nPress OK to close." % counter))) + mp.send(0) + self.__running = False + Thread.__init__(self) + +pathToDatabase = PathToDatabase() + + +class iDreamAddToDatabase(Screen): + skin = """ + + + + + + + + """ + def __init__(self, session, initDir): + Screen.__init__(self, session) + self["actions"] = ActionMap(["WizardActions", "ColorActions"], + { + "back": self.cancel, + "green": self.green, + "red": self.cancel, + "ok": self.green, + + }, -1) + self["key_red"] = StaticText(_("Cancel")) + self["key_green"] = StaticText("Close") + self["output"] = Label() + self.onClose.append(self.__onClose) + pathToDatabase.MessagePump.recv_msg.get().append(self.gotThreadMsg) + if not pathToDatabase.isRunning and initDir: + pathToDatabase.Start(initDir) + + def gotThreadMsg(self, msg): + msg = pathToDatabase.Message.pop() + self["output"].setText(msg[1]) + if msg[0] == THREAD_FINISHED: + self["key_red"].setText("") + + def green(self): + self.close() + + def cancel(self): + if pathToDatabase.isRunning: + pathToDatabase.Cancel() + + def __onClose(self): + pathToDatabase.MessagePump.recv_msg.get().remove(self.gotThreadMsg) + + +class myHTTPClientFactory(HTTPClientFactory): + def __init__(self, url, method='GET', postdata=None, headers=None, + agent="SHOUTcast", timeout=0, cookies=None, + followRedirect=1, lastModified=None, etag=None): + HTTPClientFactory.__init__(self, url, method=method, postdata=postdata, + headers=headers, agent=agent, timeout=timeout, cookies=cookies,followRedirect=followRedirect) + +def sendUrlCommand(url, contextFactory=None, timeout=60, *args, **kwargs): + scheme, host, port, path = client._parse(url) + factory = myHTTPClientFactory(url, *args, **kwargs) + reactor.connectTCP(host, port, factory, timeout=timeout) + return factory.deferred + + +class MethodArguments: + def __init__(self, method = None, arguments = None): + self.method = method + self.arguments = arguments + +class CacheList: + def __init__(self, cache = True, index = 0, listview = [], headertext = "", methodarguments = None): + self.cache = cache + self.index = index + self.listview = listview + self.headertext = headertext + self.methodarguments = methodarguments + +class Item: + def __init__(self, text = "", mode = 0, id = -1, navigator = False, artistID = 0, albumID = 0, title = "", artist = "", filename = "", bitrate = None, length = "", genre = "", track = "", date = "", album = "", playlistID = 0, genreID = 0, songID = 0, join = True, PTS = None): + self.text = text + self.mode = mode + self.navigator = navigator + self.artistID = artistID + self.albumID = albumID + self.title = title + self.artist = artist + self.filename = filename + if bitrate is not None: + if join: + self.bitrate = "%d Kbps" % bitrate + else: + self.bitrate = bitrate + else: + self.bitrate = "" + self.length = length + self.genre = genre + if track is not None: + self.track = "Track %s" % track + else: + self.track = "" + if date is not None: + if join: + self.date = " (%s)" % date + else: + self.date = date + else: + self.date = "" + self.album = album + self.playlistID = playlistID + self.genreID = genreID + self.songID = songID + self.PTS = PTS + + +def OpenDatabase(): + connectstring = os_path.join(config.plugins.merlinmusicplayer.databasepath.value ,"iDream.db") + db_exists = False + if os_path.exists(connectstring): + db_exists = True + connection = sqlite.connect(connectstring) + if not db_exists : + connection.execute('CREATE TABLE IF NOT EXISTS Songs (song_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL UNIQUE, title TEXT, artist_id INTEGER, album_id INTEGER, genre_id INTEGER, tracknumber INTEGER, bitrate INTEGER, length TEXT, track TEXT, date TEXT, lyrics TEXT);') + connection.execute('CREATE TABLE IF NOT EXISTS Artists (artist_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, artist TEXT NOT NULL UNIQUE);') + connection.execute('CREATE TABLE IF NOT EXISTS Album (album_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, album_text TEXT NOT NULL UNIQUE);') + connection.execute('CREATE TABLE IF NOT EXISTS Genre (genre_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, genre_text TEXT NOT NULL UNIQUE);') + connection.execute('CREATE TABLE IF NOT EXISTS Playlists (playlist_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, playlist_text TEXT NOT NULL);') + connection.execute('CREATE TABLE IF NOT EXISTS Playlist_Songs (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, playlist_id INTEGER NOT NULL, song_id INTEGER NOT NULL);') + connection.execute('CREATE TABLE IF NOT EXISTS CurrentSongList (ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, song_id INTEGER, filename TEXT NOT NULL, title TEXT, artist TEXT, album TEXT, genre TEXT, bitrate TEXT, length TEXT, track TEXT, date TEXT, PTS INTEGER);') + return connection + +def getID3Tags(root,filename): + audio = None + isFlac = False + isAudio = True + title = "" + genre = "" + artist = "" + album = "" + tracknr = -1 + track = None + date = None + length = "" + bitrate = None + if filename.lower().endswith(".mp3"): + try: audio = MP3(os_path.join(root,filename), ID3 = EasyID3) + except: audio = None + elif filename.lower().endswith(".flac"): + try: + audio = FLAC(os_path.join(root,filename)) + isFlac = True + except: audio = None + elif filename.lower().endswith(".m4a"): + try: audio = EasyMP4(os_path.join(root,filename)) + except: audio = None + elif filename.lower().endswith(".ogg"): + try: audio = OggVorbis(os_path.join(root,filename)) + except: audio = None + else: + isAudio = False + if audio: + title = audio.get('title', [filename])[0] + genre = audio.get('genre', ['n/a'])[0] + artist = audio.get('artist', ['n/a'])[0] + album = audio.get('album', ['n/a'])[0] + tracknr = int(audio.get('tracknumber', ['-1'])[0].split("/")[0]) + track = audio.get('tracknumber', [None])[0] + date = audio.get('date', [None])[0] + + length = str(datetime.timedelta(seconds=int(audio.info.length))) + if not isFlac: + bitrate = audio.info.bitrate / 1000 + else: + bitrate = None + else: + if isAudio: + title = os_path.splitext(os_path.basename(filename))[0] + genre = "n/a" + artist = "n/a" + album = "n/a" + tracknr = -1 + track = None + date = None + length = "" + bitrate = None + + return audio, isAudio, title.encode("utf-8", 'ignore'), genre.encode("utf-8", 'ignore'),artist.encode("utf-8", 'ignore'),album.encode("utf-8", 'ignore'),tracknr,track,date,length.encode("utf-8", 'ignore'),bitrate + +class MerlinMusicPlayerScreenSaver(Screen): + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + """ + elif sz_w == 1024: + skin = """ + + + + """ + + else: + skin = """ + + + + """ + + + def __init__(self, session): + self.session = session + Screen.__init__(self, session) + self["actions"] = ActionMap(["WizardActions", "DirectionActions", "ColorActions", "EventViewActions"], + { + "back": self.close, + "right": self.close, + "left": self.close, + "up": self.close, + "down": self.close, + "ok": self.close, + "pageUp": self.close, + "pageDown": self.close, + "yellow": self.close, + "blue": self.close, + "red": self.close, + "green": self.close, + "right": self.close, + "left": self.close, + "prevBouquet": self.close, + "nextBouquet": self.close, + "info": self.close, + + }, -1) + self["coverArt"] = MerlinMediaPixmap() + self.coverMoveTimer = eTimer() + self.coverMoveTimer.timeout.get().append(self.moveCoverArt) + self.coverMoveTimer.start(1) + self["display"] = Label() + + def updateDisplayText(self, text): + self["display"].setText(text) + + def updateLCD(self, text, line): + self.summaries.setText(text,line) + + def updateCover(self, filename = None, modus = 0): + print "[MerlinMusicPlayerScreenSaver] updating coverart with filename = %s and modus = %d" % (filename, modus) + if modus == 0: + if filename: + self["coverArt"].showCoverFromFile(filename) + else: + self["coverArt"].showDefaultCover() + elif modus == 1: + self["coverArt"].showDefaultCover() + elif modus == 2: + self["coverArt"].embeddedCoverArt() + elif modus == 3: + self["coverArt"].updateCoverArt(filename) + elif modus == 4: + self["coverArt"].showCoverFromFile(filename) + + def moveCoverArt(self): + x = randrange(getDesktop(0).size().width()-238) + y = randrange(getDesktop(0).size().height()-238-28) + self["coverArt"].move(ePoint(x,y)) + self["display"].move(ePoint(x,y+240)) + self.coverMoveTimer.start(15000) + + def createSummary(self): + return MerlinMusicPlayerLCDScreen + +class MerlinMusicPlayerScreen(Screen, InfoBarBase, InfoBarSeek, InfoBarNotifications): + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + + + + + + + + + + + + Length,ShowHours + + + Position,ShowHours + + + + + """ + elif sz_w == 1024: + skin = """ + + + + + + + + + + + + + + + Length,ShowHours + + + Position,ShowHours + + + + + """ + else: + skin = """ + + + + + + + + + + + + + + + Length,ShowHours + + + Position,ShowHours + + + + + """ + + + def __init__(self, session, songlist, index, idreammode): + self.session = session + Screen.__init__(self, session) + InfoBarNotifications.__init__(self) + InfoBarBase.__init__(self) + self["actions"] = ActionMap(["WizardActions", "MediaPlayerActions", "EPGSelectActions", "MediaPlayerSeekActions", "ColorActions"], + { + "back": self.closePlayer, + "pause": self.pauseEntry, + "stop": self.stopEntry, + "right": self.playNext, + "left": self.playPrevious, + "up": self.showPlaylist, + "down" : self.showPlaylist, + "prevBouquet": self.shuffleList, + "nextBouquet": self.repeatSong, + "info" : self.showLyrics, + "yellow": self.pauseEntry, + "green": self.play, + "input_date_time": self.config, + }, -1) + + self.onClose.append(self.__onClose) + self.session.nav.stopService() + self["PositionGauge"] = ServicePositionGauge(self.session.nav) + self["coverArt"] = MerlinMediaPixmap() + self["repeat"] = MultiPixmap() + self["shuffle"] = MultiPixmap() + self["dvrStatus"] = MultiPixmap() + self["title"] = Label() + self["album"] = Label() + self["artist"] = Label() + self["genre"] = Label() + self["nextTitle"] = Label() + self.__event_tracker = ServiceEventTracker(screen=self, eventmap= + { + iPlayableService.evUpdatedInfo: self.__evUpdatedInfo, + iPlayableService.evUser+10: self.__evAudioDecodeError, + iPlayableService.evUser+12: self.__evPluginError, + iPlayableService.evUser+13: self.embeddedCoverArt, + iPlayableService.evStart: self.__serviceStarted, + }) + + InfoBarSeek.__init__(self, actionmap = "MediaPlayerSeekActions") + self.songList = songlist + self.origSongList = songlist[:] + self.currentIndex = index + self.shuffle = False + self.repeat = False + self.currentFilename = "" + self.currentGoogleCoverFile = "" + self.googleDownloadDir = os_path.join(config.plugins.merlinmusicplayer.googleimagepath.value, "downloaded_covers/" ) + if not os_path.exists(self.googleDownloadDir): + os_mkdir(self.googleDownloadDir) + self.init = 0 + self.onShown.append(self.__onShown) + # for lcd + self.currentTitle = "" + self.nextTitle = "" + self.screenSaverTimer = eTimer() + self.screenSaverTimer.timeout.get().append(self.screenSaverTimerTimeout) + self.screenSaverScreen = None + + self.iDreamMode = idreammode + + def embeddedCoverArt(self): + self["coverArt"].embeddedCoverArt() + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(modus = 2) + + def screenSaverTimerTimeout(self): + if config.plugins.merlinmusicplayer.usescreensaver.value: + if self.screenSaverTimer.isActive(): + self.screenSaverTimer.stop() + if not self.screenSaverScreen: + self.screenSaverScreen = self.session.instantiateDialog(MerlinMusicPlayerScreenSaver) + self.session.execDialog(self.screenSaverScreen) + self.screenSaverScreen.updateLCD(self.currentTitle,1) + self.screenSaverScreen.updateLCD(self.nextTitle,4) + album = self["album"].getText() + if album: + text = "%s - %s" % (self["title"].getText(), album) + else: + text = self["title"].getText() + self.screenSaverScreen.updateDisplayText(text) + self.screenSaverScreen.updateCover(self["coverArt"].coverArtFileName, modus = 0) + + def resetScreenSaverTimer(self): + if config.plugins.merlinmusicplayer.usescreensaver.value and config.plugins.merlinmusicplayer.screensaverwait.value != 0: + if self.screenSaverTimer.isActive(): + self.screenSaverTimer.stop() + self.screenSaverTimer.start(config.plugins.merlinmusicplayer.screensaverwait.value * 60000) + + def __onShown(self): + if self.init == 0: + self.init = 1 + self["coverArt"].onShow() + self.playSong(self.songList[self.currentIndex][0].filename) + else: + self.summaries.setText(self.currentTitle,1) + self.summaries.setText(self.nextTitle,4) + if self.screenSaverScreen: + self.screenSaverScreen.doClose() + self.screenSaverScreen = None + self.resetScreenSaverTimer() + + def __onClose(self): + del self["coverArt"].picload + self.seek = None + + def config(self): + if self.screenSaverTimer.isActive(): + self.screenSaverTimer.stop() + self.session.openWithCallback(self.setupFinished, MerlinMusicPlayerSetup, False) + + def setupFinished(self, result): + if result: + self.googleDownloadDir = os_path.join(config.plugins.merlinmusicplayer.googleimagepath.value, "downloaded_covers/" ) + if not os_path.exists(self.googleDownloadDir): + os_mkdir(self.googleDownloadDir) + self.resetScreenSaverTimer() + + def closePlayer(self): + if config.plugins.merlinmusicplayer.startlastsonglist.value: + config.plugins.merlinmusicplayer.lastsonglistindex.value = self.currentIndex + config.plugins.merlinmusicplayer.lastsonglistindex.save() + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + cursor.execute("Delete from CurrentSongList;") + for song in self.origSongList: + cursor.execute("INSERT INTO CurrentSongList (song_id, filename,title,artist,album,genre, bitrate, length, track, date, PTS) VALUES(?,?,?,?,?,?,?,?,?,?,?);" , (song[0].songID, song[0].filename,song[0].title,song[0].artist,song[0].album,song[0].genre, song[0].bitrate, song[0].length, song[0].track, song[0].date, song[0].PTS)) + connection.commit() + cursor.close() + connection.close() + if self.screenSaverTimer.isActive(): + self.screenSaverTimer.stop() + self.close() + + def playSong(self, filename): + self.session.nav.stopService() + self.seek = None + self.currentFilename = filename + if not config.plugins.merlinmusicplayer.hardwaredecoder.value and self.currentFilename.lower().endswith(".mp3") and self.songList[self.currentIndex][0].PTS is None: + sref = eServiceReference(ENIGMA_MERLINPLAYER_ID, 0, self.currentFilename) # play mp3 file with merlinmp3player lib + self.session.nav.playService(sref) + if self.iDreamMode: + self.updateMusicInformation( self.songList[self.currentIndex][0].artist, self.songList[self.currentIndex][0].title, + self.songList[self.currentIndex][0].album, self.songList[self.currentIndex][0].genre, self.songList[self.currentIndex][0].date, clear = True ) + else: + path,filename = os_path.split(self.currentFilename) + audio, isAudio, title, genre,artist,album,tracknr,track,date,length,bitrate = getID3Tags(path,filename) + if audio: + if date: + year = "(%s)" % str(date) + else: + year = "" + self.updateMusicInformation( artist, title, album, genre, year, clear = True ) + else: + self.updateMusicInformation( title = title, clear = True) + audio = None + else: + sref = eServiceReference(4097, 0, self.currentFilename) + self.session.nav.playService(sref) + if self.songList[self.currentIndex][0].PTS is not None: + service = self.session.nav.getCurrentService() + if service: + self.seek = service.seek() + self.updateMusicInformationCUE() + self.ptsTimer = eTimer() + self.ptsTimer.callback.append(self.ptsTimerCallback) + self.ptsTimer.start(1000) + self["nextTitle"].setText(self.getNextTitle()) + + def ptsTimerCallback(self): + if self.seek: + pts = self.seek.getPlayPosition() + index = 0 + currentIndex = 0 + for songs in self.songList: + if pts[1] > songs[0].PTS: + currentIndex = index + else: + break + index +=1 + if currentIndex != self.currentIndex: + self.currentIndex = currentIndex + self.updateMusicInformationCUE() + self.ptsTimer.start(1000) + + def updateMusicInformationCUE(self): + self.updateSingleMusicInformation("artist", self.songList[self.currentIndex][0].artist, True) + self.updateSingleMusicInformation("title", self.songList[self.currentIndex][0].title, True) + self.updateSingleMusicInformation("album", self.songList[self.currentIndex][0].album, True) + self.summaries.setText(self.songList[self.currentIndex][0].title,1) + if self.screenSaverScreen: + self.screenSaverScreen.updateLCD(self.songList[self.currentIndex][0].title,1) + if self.songList[self.currentIndex][0].album: + self.screenSaverScreen.updateDisplayText("%s - %s" % (self.songList[self.currentIndex][0].title,self.songList[self.currentIndex][0].album)) + else: + self.screenSaverScreen.updateDisplayText(self.songList[self.currentIndex][0].title) + self.updateCover(self.songList[self.currentIndex][0].artist, self.songList[self.currentIndex][0].album) + self.currentTitle = self.songList[self.currentIndex][0].title + self["nextTitle"].setText(self.getNextTitle()) + + def __serviceStarted(self): + self["dvrStatus"].setPixmapNum(0) + + def __evUpdatedInfo(self): + currPlay = self.session.nav.getCurrentService() + if currPlay is not None: + sTitle = currPlay.info().getInfoString(iServiceInformation.sTagTitle) + sAlbum = currPlay.info().getInfoString(iServiceInformation.sTagAlbum) + sArtist = currPlay.info().getInfoString(iServiceInformation.sTagArtist) + sGenre = currPlay.info().getInfoString(iServiceInformation.sTagGenre) + sYear = currPlay.info().getInfoString(iServiceInformation.sTagDate) + if sYear: + sYear = "(%s)" % sYear + if not sTitle: + sTitle = os_path.splitext(os_path.basename(self.currentFilename))[0] + + if self.songList[self.currentIndex][0].PTS is None: + self.updateMusicInformation( sArtist, sTitle, sAlbum, sGenre, sYear, clear = True ) + else: + self.updateSingleMusicInformation("genre", sGenre, True) + else: + self.updateMusicInformation() + + def updateMusicInformation(self, artist = "", title = "", album = "", genre = "", year = "", clear = False): + if year and album: + album = "%s %s" % (album, year) + self.updateSingleMusicInformation("artist", artist, clear) + self.updateSingleMusicInformation("title", title, clear) + self.updateSingleMusicInformation("album", album, clear) + self.updateSingleMusicInformation("genre", genre, clear) + self.currentTitle = title + if not self.iDreamMode and self.songList[self.currentIndex][0].PTS is None: + # for lyrics + self.songList[self.currentIndex][0].title = title + self.songList[self.currentIndex][0].artist = artist + self.summaries.setText(title,1) + if self.screenSaverScreen: + self.screenSaverScreen.updateLCD(title,1) + if album: + self.screenSaverScreen.updateDisplayText("%s - %s" % (title,album)) + else: + self.screenSaverScreen.updateDisplayText(title) + self.updateCover(artist, album) + + def updateCover(self, artist, album): + hasCover = False + audio = None + audiotype = 0 + if self.currentFilename.lower().endswith(".mp3"): + try: + audio = ID3(self.currentFilename) + audiotype = 1 + except: audio = None + elif self.currentFilename.lower().endswith(".flac"): + try: + audio = FLAC(self.currentFilename) + audiotype = 2 + except: audio = None + elif self.currentFilename.lower().endswith(".m4a"): + try: + audio = MP4(self.currentFilename) + audiotype = 3 + except: audio = None + elif self.currentFilename.lower().endswith(".ogg"): + try: + audio = OggVorbis(self.currentFilename) + audiotype = 4 + except: audio = None + if audio: + if audiotype == 1: + apicframes = audio.getall("APIC") + if len(apicframes) >= 1: + hasCover = True + if not config.plugins.merlinmusicplayer.hardwaredecoder.value: + coverArtFile = file("/tmp/.id3coverart", 'wb') + coverArtFile.write(apicframes[0].data) + coverArtFile.close() + self["coverArt"].embeddedCoverArt() + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(modus = 2) + elif audiotype == 2: + if len(audio.pictures) >= 1: + hasCover = True + elif audiotype == 3: + if 'covr' in audio.tags: + hasCover = True + elif audiotype == 4: + if 'METADATA_BLOCK_PICTURE' in audio.tags: + hasCover = True + audio = None + if not hasCover: + if not self["coverArt"].updateCoverArt(self.currentFilename): + if config.plugins.merlinmusicplayer.usegoogleimage.value: + self.getGoogleCover(artist, album) + else: + self["coverArt"].showDefaultCover() + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(modus = 1) + else: + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(filename = self.currentFilename, modus = 3) + self.currentGoogleCoverFile = "" + else: + self.currentGoogleCoverFile = "" + + def updateSingleMusicInformation(self, name, info, clear): + if info != "" or clear: + if self[name].getText() != info: + self[name].setText(info) + + def getGoogleCover(self, artist,album): + if artist != "" and album != "": + url = "http://images.google.de/images?q=%s+%s&btnG=Bilder-Suche" % (quote(album),quote(artist)) + sendUrlCommand(url, None,10).addCallback(self.googleImageCallback).addErrback(self.coverDownloadFailed) + else: + self["coverArt"].showDefaultCover() + + def googleImageCallback(self, result): + foundPos = result.find("imgres?imgurl=") + foundPos2 = result.find("&imgrefurl=") + if foundPos != -1 and foundPos2 != -1: + url = result[foundPos+14:foundPos2] + parts = url.split("/") + filename = parts[-1] + if filename != self.currentGoogleCoverFile: + self.currentGoogleCoverFile = filename + filename = self.googleDownloadDir + parts[-1] + if os_path.exists(filename): + print "[MerlinMusicPlayer] using cover from %s " % filename + self["coverArt"].showCoverFromFile(filename) + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(filename = filename, modus = 4) + else: + print "[MerlinMusicPlayer] downloading cover from %s " % url + downloadPage(url , self.googleDownloadDir + parts[-1]).addCallback(boundFunction(self.coverDownloadFinished, filename)).addErrback(self.coverDownloadFailed) + + def coverDownloadFailed(self,result): + print "[MerlinMusicPlayer] cover download failed: %s " % result + self["coverArt"].showDefaultCover() + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(modus = 1) + + def coverDownloadFinished(self,filename, result): + print "[MerlinMusicPlayer] cover download finished" + self["coverArt"].showCoverFromFile(filename) + if self.screenSaverScreen: + self.screenSaverScreen.updateCover(filename = filename, modus = 4) + + def __evAudioDecodeError(self): + currPlay = self.session.nav.getCurrentService() + sAudioType = currPlay.info().getInfoString(iServiceInformation.sUser+10) + print "[MerlinMusicPlayer] audio-codec %s can't be decoded by hardware" % (sAudioType) + self.session.open(MessageBox, _("This Dreambox can't decode %s streams!") % sAudioType, type = MessageBox.TYPE_INFO,timeout = 20 ) + + def __evPluginError(self): + currPlay = self.session.nav.getCurrentService() + message = currPlay.info().getInfoString(iServiceInformation.sUser+12) + print "[MerlinMusicPlayer]" , message + self.session.open(MessageBox, message, type = MessageBox.TYPE_INFO,timeout = 20 ) + + def doEofInternal(self, playing): + if playing: + self.playNext() + + def checkSkipShowHideLock(self): + self.updatedSeekState() + + def updatedSeekState(self): + if self.seekstate == self.SEEK_STATE_PAUSE: + self["dvrStatus"].setPixmapNum(1) + elif self.seekstate == self.SEEK_STATE_PLAY: + self["dvrStatus"].setPixmapNum(0) + + def pauseEntry(self): + self.pauseService() + self.resetScreenSaverTimer() + + def play(self): + #play the current song from beginning again + if self.songList[self.currentIndex][0].PTS is None: + self.playSong(self.songList[self.currentIndex][0].filename) + else: + if self.seek: + self.seek.seekTo(self.songList[self.currentIndex][0].PTS) + self.updatedSeekState() + self.resetScreenSaverTimer() + + def unPauseService(self): + self.setSeekState(self.SEEK_STATE_PLAY) + + def stopEntry(self): + self.seek = None + self.session.nav.stopService() + self.origSongList = [] + self.songList = [] + if config.plugins.merlinmusicplayer.startlastsonglist.value: + config.plugins.merlinmusicplayer.lastsonglistindex.value = -1 + config.plugins.merlinmusicplayer.lastsonglistindex.save() + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + cursor.execute("Delete from CurrentSongList;") + connection.commit() + cursor.close() + connection.close() + self.resetScreenSaverTimer() + self.close() + + def playNext(self): + if not self.repeat: + if self.currentIndex +1 > len(self.songList) -1: + self.currentIndex = 0 + else: + self.currentIndex += 1 + if self.songList[self.currentIndex][0].PTS is None: + self.playSong(self.songList[self.currentIndex][0].filename) + else: + self.playCUETrack() + if not self.screenSaverScreen: + self.resetScreenSaverTimer() + + def playPrevious(self): + if not self.repeat: + if self.currentIndex - 1 < 0: + self.currentIndex = len(self.songList) - 1 + else: + self.currentIndex -= 1 + + if self.songList[self.currentIndex][0].PTS is None: + self.playSong(self.songList[self.currentIndex][0].filename) + else: + self.playCUETrack() + self.resetScreenSaverTimer() + + def getNextTitle(self): + if self.repeat: + index = self.currentIndex + else: + if self.currentIndex + 1 > len(self.songList) -1: + index = 0 + else: + index = self.currentIndex + 1 + if self.iDreamMode or self.songList[index][0].PTS is not None: + text = "%s - %s" % (self.songList[index][0].title, self.songList[index][0].artist) + else: + if self.songList[index][0].filename.lower().startswith("http://"): + text = self.songList[index][0].filename + else: + path,filename = os_path.split(self.songList[index][0].filename) + audio, isAudio, title, genre,artist,album,tracknr,track,date,length,bitrate = getID3Tags(path,filename) + if audio: + if artist: + text = "%s - %s" % (title, artist) + else: + text = title + else: + text = title + audio = None + self.nextTitle = text + self.summaries.setText(text,4) + if self.screenSaverScreen: + self.screenSaverScreen.updateLCD(text,4) + return str(text) + + def shuffleList(self): + if self.songList[self.currentIndex][0].PTS is None: # not implemented for cue files yet + self.shuffle = not self.shuffle + if self.shuffle: + self["shuffle"].setPixmapNum(1) + shuffle(self.songList) + else: + self.songList = self.origSongList[:] + self["shuffle"].setPixmapNum(0) + index = 0 + for x in self.songList: + if x[0].filename == self.currentFilename: + self.currentIndex = index + break + index += 1 + self["nextTitle"].setText(self.getNextTitle()) + else: + self.session.open(MessageBox, _("Shuffle is not available yet with cue-files!"), type = MessageBox.TYPE_INFO,timeout = 20 ) + self.resetScreenSaverTimer() + + def repeatSong(self): + if self.songList[self.currentIndex][0].PTS is None: # not implemented for cue files yet + self.repeat = not self.repeat + if self.repeat: + self["repeat"].setPixmapNum(1) + else: + self["repeat"].setPixmapNum(0) + self["nextTitle"].setText(self.getNextTitle()) + else: + self.session.open(MessageBox, _("Repeat is not available yet with cue-files!"), type = MessageBox.TYPE_INFO,timeout = 20 ) + self.resetScreenSaverTimer() + + def showPlaylist(self): + if self.screenSaverTimer.isActive(): + self.screenSaverTimer.stop() + self.session.openWithCallback(self.showPlaylistCallback, MerlinMusicPlayerSongList, self.songList, self.currentIndex, self.iDreamMode) + + def showPlaylistCallback(self, index): + if index != -1: + self.currentIndex = index + + if self.songList[self.currentIndex][0].PTS is None: + self.playSong(self.songList[self.currentIndex][0].filename) + else: + self.playCUETrack() + + self.resetScreenSaverTimer() + + def playCUETrack(self): + if self.ptsTimer.isActive(): + self.ptsTimer.stop() + if self.seek: + self.seek.seekTo(self.songList[self.currentIndex][0].PTS) + self.updatedSeekState() + self.updateMusicInformationCUE() + self.ptsTimer.start(1000) + + def showLyrics(self): + if self.screenSaverTimer.isActive(): + self.screenSaverTimer.stop() + self.session.openWithCallback(self.resetScreenSaverTimer, MerlinMusicPlayerLyrics, self.songList[self.currentIndex][0]) + + def createSummary(self): + return MerlinMusicPlayerLCDScreen + +class MerlinMusicPlayerLyrics(Screen): + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + + + + + + """ + elif sz_w == 1024: + skin = """ + + + + + + + + """ + else: + skin = """ + + + + + + + + """ + + + def __init__(self, session, currentsong): + self.session = session + Screen.__init__(self, session) + self["headertext"] = Label(_("Merlin Music Player Lyrics")) + self["resulttext"] = Label(_("Getting lyrics from api.leoslyrics.com...")) + self["actions"] = ActionMap(["WizardActions", "DirectionActions"], + { + "back": self.close, + "upUp": self.pageUp, + "leftUp": self.pageUp, + "downUp": self.pageDown, + "rightUp": self.pageDown, + }, -1) + self["lyric_text"] = ScrollLabel() + self.currentSong = currentsong + self.onLayoutFinish.append(self.startRun) + + def startRun(self): + url = "http://api.leoslyrics.com/api_search.php?auth=duane&artist=%s&songtitle=%s" % (quote(self.currentSong.artist), quote(self.currentSong.title)) + sendUrlCommand(url, None,10).addCallback(self.getHID).addErrback(self.urlError) + + def urlError(self, error = None): + if error is not None: + self["resulttext"].setText(str(error.getErrorMessage())) + + def getHID(self, xmlstring): + root = cet_fromstring(xmlstring) + url = "" + child = root.find("searchResults") + if child: + child2 = child.find("result") + if child2: + url = "http://api.leoslyrics.com/api_lyrics.php?auth=duane&hid=%s" % quote(child2.get("hid")) + sendUrlCommand(url, None,10).addCallback(self.getLyrics).addErrback(self.urlError) + if not url: + self["resulttext"].setText(_("No lyrics found")) + + def getLyrics(self, xmlstring): + root = cet_fromstring(xmlstring) + lyrictext = "" + child = root.find("lyric") + if child: + title = child.findtext("title").encode("utf-8", 'ignore') + child2 = child.find("artist") + if child2: + artist = child2.findtext("name").encode("utf-8", 'ignore') + else: + artist = "" + lyrictext = child.findtext("text").encode("utf-8", 'ignore') + self["lyric_text"].setText(lyrictext) + result = _("Response -> lyrics for: %s (%s)") % (title,artist) + self["resulttext"].setText(result) + if not lyrictext: + self["resulttext"].setText(_("No lyrics found")) + + def pageUp(self): + self["lyric_text"].pageUp() + + def pageDown(self): + self["lyric_text"].pageDown() + +class MerlinMusicPlayerSongList(Screen): + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + + + + + """ + elif sz_w == 1024: + skin = """ + + + + + + + """ + else: + skin = """ + + + + + + + """ + + + def __init__(self, session, songlist, index, idreammode): + self.session = session + Screen.__init__(self, session) + self["headertext"] = Label(_("Merlin Music Player Songlist")) + self["list"] = iDreamList() + self["list"].connectSelChanged(self.lcdUpdate) + self["actions"] = ActionMap(["WizardActions"], + { + "ok": self.ok, + "back": self.closing, + }, -1) + self.songList = songlist + self.index = index + self.iDreamMode = idreammode + self.onLayoutFinish.append(self.startRun) + self.onShown.append(self.lcdUpdate) + + def startRun(self): + if self.iDreamMode: + self["list"].setMode(10) # songlist + self["list"].setList(self.songList) + self["list"].moveToIndex(self.index) + + def ok(self): + self.close(self["list"].getCurrentIndex()) + + def closing(self): + self.close(-1) + + def lcdUpdate(self): + try: + index = self["list"].getCurrentIndex() + songlist = self["list"].getList() + self.summaries.setText(songlist[index][0].title,1) + count = self["list"].getItemCount() + # voheriges + index -= 1 + if index < 0: + index = count + self.summaries.setText(songlist[index][0].title,3) + # naechstes + index = self["list"].getCurrentIndex() + 1 + if index > count: + index = 0 + self.summaries.setText(songlist[index][0].title,4) + except: pass + + def createSummary(self): + return MerlinMusicPlayerLCDScreenText + +class iDreamMerlin(Screen): + + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + + + + + + + + + + """ + elif sz_w == 1024: + skin = """ + + + + + + + + + + + + """ + else: + skin = """ + + + + + + + + + + + + """ + + + def __init__(self, session): + self.session = session + Screen.__init__(self, session) + self["list"] = iDreamList() + self["list"].connectSelChanged(self.lcdUpdate) + + + self["actions"] = ActionMap(["WizardActions", "DirectionActions", "ColorActions", "EPGSelectActions"], + { + "ok": self.ok, + "back": self.closing, + "red": self.red_pressed, + "green": self.green_pressed, + "yellow": self.yellow_pressed, + "blue": self.blue_pressed, + "input_date_time": self.menu_pressed, + "info" : self.info_pressed, + }, -1) + + self["actions2"] = NumberActionMap(["InputActions"], + { + "0": self.keyNumber_pressed, + }, -1) + + self.onLayoutFinish.append(self.startRun) + self.onShown.append(self.lcdUpdate) + self.onClose.append(self.__onClose) + + self.CurrentService = self.session.nav.getCurrentlyPlayingServiceReference() + self.session.nav.stopService() + + self.mode = 0 + self.mainMenuList = [] + self.cacheList = [] + self.LastMethod = None + self.player = None + + self["key_red"] = StaticText("") + self["key_green"] = StaticText("") + self["key_yellow"] = StaticText("") + self["key_blue"] = StaticText("") + self["headertext"] = Label(_("iDream Main Menu")) + + def getPlayList(self): + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + playList = [] + cursor.execute("select playlist_id,playlist_text from playlists order by playlist_text;") + for row in cursor: + playList.append((row[1], row[0])) + cursor.close() + connection.close() + return playList + + def sqlCommand(self, sqlSatement): + connection = OpenDatabase() + cursor = connection.cursor() + cursor.execute(sqlSatement) + cursor.close() + connection.commit() + connection.close() + + def clearCache(self): + for items in self.cacheList: + items.cache = False + items.listview = [] + items.headertext = "" + + def getCurrentSelection(self): + sel = None + try: sel = self["list"].l.getCurrentSelection()[0] + except: pass + return sel + + def addListToPlaylistConfirmed(self, methodName, answer): + if answer: + playList = self.getPlayList() + if len(playList): + self.session.openWithCallback(methodName, ChoiceBox,list = playList) + else: + self.session.openWithCallback(self.createPlaylistConfirmed, MessageBox, _("There are no playlists defined.\nDo you want to create a new playlist?")) + + def menu_pressed(self): + options = [(_("Configuration"), self.config),(_("Search in iDream database"), self.searchInIDreamDatabase),] + options.extend(((_("Scan path for music files and add them to database"), self.scanDir),)) + if self.mode != 1: + options.extend(((_("Create new playlist"), self.createPlaylist),)) + if self["list"].getDisplaySongMode(): + if self.mode == 2: + options.extend(((_("Delete song from current playlist"), self.deleteSongFromPlaylist),)) + else: + options.extend(((_("Add selected song to a playlist"), self.addSongToPlaylist),)) + if self.mode == 18: + options.extend(((_("Add all songs from selected album to a playlist"), self.addAlbumToPlaylist),)) + elif self.mode == 19: + options.extend(((_("Add all songs from selected artist to a playlist"), self.addArtistToPlaylist),)) + options.extend(((_("Delete song from database"), self.deleteSongFromDatabase),)) + else: + if self.mode == 1: + options.extend(((_("Delete selected playlist"), self.deletePlaylist),)) + elif self.mode == 4: + options.extend(((_("Add all songs from selected artist to a playlist"), self.addArtistToPlaylist),)) + elif self.mode == 5 or self.mode == 7: + options.extend(((_("Add all songs from selected album to a playlist"), self.addAlbumToPlaylist),)) + elif self.mode == 13: + options.extend(((_("Add all songs from selected genre to a playlist"), self.addGenreToPlaylist),)) + self.session.openWithCallback(self.menuCallback, ChoiceBox,list = options) + + def menuCallback(self, ret): + ret and ret[1]() + + def scanDir(self): + SelectPath + self.session.openWithCallback(self.pathSelected,SelectPath,"/media/") + + def pathSelected(self, res): + if res is not None: + self.session.openWithCallback(self.filesAdded, iDreamAddToDatabase,res) + + def filesAdded(self): + if pathToDatabase.isRunning: + self.close() + else: + self.red_pressed() + + + + + def addGenreToPlaylist(self): + self.session.openWithCallback(boundFunction(self.addListToPlaylistConfirmed,self.addGenreToPlaylistConfirmedCallback), MessageBox, _("Do you really want to add all songs from that genre to a playlist?")) + + def addGenreToPlaylistConfirmedCallback(self, ret): + if ret: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("INSERT INTO Playlist_Songs (playlist_id,song_id) select %d, song_id from songs where genre_id=%d order by album_id,tracknumber,title,filename;" % (ret[1],sel.genreID)) + self.clearCache() + + def addArtistToPlaylist(self): + self.session.openWithCallback(boundFunction(self.addListToPlaylistConfirmed, self.addArtistToPlaylistConfirmedCallback), MessageBox, _("Do you really want to add all songs from that artist to a playlist?")) + + def addArtistToPlaylistConfirmedCallback(self, ret): + if ret: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("INSERT INTO Playlist_Songs (playlist_id,song_id) select %d, song_id from songs where artist_id=%d order by album_id,tracknumber,title,filename;" % (ret[1],sel.artistID)) + self.clearCache() + + def addAlbumToPlaylist(self): + self.session.openWithCallback(boundFunction(self.addListToPlaylistConfirmed, self.addAlbumToPlaylistConfirmedCallback), MessageBox, _("Do you really want to add all songs from that album to a playlist?")) + + def addAlbumToPlaylistConfirmedCallback(self, ret): + if ret: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("INSERT INTO Playlist_Songs (playlist_id,song_id) select %d, song_id from songs where album_id=%d order by tracknumber,title,filename;" % (ret[1],sel.albumID)) + self.clearCache() + + def deletePlaylist(self): + self.session.openWithCallback(self.deletePlaylistConfirmed, MessageBox, _("Do you really want to delete the current playlist?")) + + def deletePlaylistConfirmed(self, answer): + if answer: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("delete from playlist_songs where playlist_id = %d" % (sel.playlistID)) + self.sqlCommand("delete from playlists where playlist_id = %d" % (sel.playlistID)) + self["list"].removeItem(self["list"].getCurrentIndex()) + self.clearCache() + + + def deleteSongFromPlaylist(self): + self.session.openWithCallback(self.deleteSongFromPlaylistConfirmed, MessageBox, _("Do you really want to delete that song the current playlist?")) + + def deleteSongFromPlaylistConfirmed(self, answer): + if answer: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("delete from playlist_songs where song_id = %d" % (sel.songID)) + self["list"].removeItem(self["list"].getCurrentIndex()) + self.clearCache() + + def deleteSongFromDatabase(self): + self.session.openWithCallback(self.deleteSongFromDatabaseConfirmed, MessageBox, _("Do you really want to delete that song from the database?")) + + def deleteSongFromDatabaseConfirmed(self, answer): + if answer: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("delete from playlist_songs where song_id = %d" % (sel.songID)) + self.sqlCommand("delete from songs where song_id = %d" % (sel.songID)) + self["list"].removeItem(self["list"].getCurrentIndex()) + self.clearCache() + + def addSongToPlaylist(self): + playList = self.getPlayList() + if len(playList): + self.session.openWithCallback(self.addSongToPlaylistCallback, ChoiceBox,list = playList) + else: + self.session.openWithCallback(self.createPlaylistConfirmed, MessageBox, _("There are no playlists defined.\nDo you want to create a new playlist?")) + + def createPlaylistConfirmed(self, val): + if val: + self.createPlaylist() + + def addSongToPlaylistCallback(self,ret): + if ret: + sel = self.getCurrentSelection() + if sel: + self.sqlCommand("INSERT INTO Playlist_Songs (playlist_id,song_id) VALUES(%d,%d);" % (ret[1],sel.songID)) + self.clearCache() + + def createPlaylist(self): + self.session.openWithCallback(self.createPlaylistFinished, VirtualKeyBoard, title = _("Enter name for playlist")) + + def createPlaylistFinished(self, text = None): + if text: + self.sqlCommand("""INSERT INTO Playlists (playlist_text) VALUES("%s");""" % (text)) + self.clearCache() + self.menu_pressed() + + def searchInIDreamDatabase(self): + options = [(_("search for title"), 1), + (_("search for artist"), 2), + (_("search for album"), 3), + (_("search in all of them"), 4),] + self.session.openWithCallback(self.enterSearchText, ChoiceBox,list = options) + + def enterSearchText(self, ret): + if ret: + self.session.openWithCallback(boundFunction(self.enterSearchTextFinished,ret[1]), VirtualKeyBoard, title = _("Enter search-text")) + + def enterSearchTextFinished(self, searchType, searchText = None): + if searchText: + search = "%" + searchText + "%" + if searchType == 1: + sql_where = "where title like '%s'" % search + text = _("""Search results for "%s" in all titles""") % searchText + elif searchType == 2: + sql_where = "where artists.artist like '%s'" % search + text = _("""Search results for "%s" in all artists""") % searchText + elif searchType == 3: + sql_where = "where album_text like '%s'" % search + text = _("""Search results for "%s" in all albums""") % searchText + else: + sql_where = "where (title like '%s' or artists.artist like '%s' or album_text like '%s')" % (search,search,search) + text = _("""Search results for "%s" in title, artist or album""") % searchText + self.setButtons(red = True, yellow = True, blue = True) + oldmode = self.mode + self.mode = 20 + self["list"].setMode(self.mode) + self.buildSearchSongList(sql_where, text, oldmode, True) + + + def keyNumber_pressed(self, number): + if number == 0 and self.mode != 0: + self["list"].moveToIndex(0) + self.ok() + + def ok(self): + sel = self.getCurrentSelection() + if sel is None: + return + if sel.mode == 99: + self.green_pressed() + else: + self.mode = sel.mode + self["list"].setMode(self.mode) + if sel.navigator and len(self.cacheList) > 0: + cache = self.cacheList.pop() + else: + cache = CacheList(cache = False, index = -1) + if sel.navigator: + self["headertext"].setText(cache.headertext) + if cache.cache: + self["list"].setList(cache.listview) + self.LastMethod = MethodArguments(method = cache.methodarguments.method, arguments = cache.methodarguments.arguments) + else: + cache.methodarguments.method(**cache.methodarguments.arguments) + self["list"].moveToIndex(cache.index) + if self.mode == 0: + self.setButtons() + if not sel.navigator: + self.buildMainMenuList() + elif self.mode == 1: + self.setButtons(red = True) + if not sel.navigator: + self.buildPlaylistList(addToCache = True) + elif self.mode == 2: + self.setButtons(red = True, green = True, yellow = True, blue = True) + if not sel.navigator: + self.buildPlaylistSongList(playlistID = sel.playlistID, addToCache = True) + elif self.mode == 4: + self.setButtons(red = True) + if not sel.navigator: + self.buildArtistList(addToCache = True) + elif self.mode == 5: + self.setButtons(red = True) + if not sel.navigator: + self.buildArtistAlbumList(sel.artistID, addToCache = True) + elif self.mode == 6: + self.setButtons(red = True, green = True, yellow = True) + if not sel.navigator: + self.buildAlbumSongList(albumID = sel.albumID, mode = 5, addToCache = True) + elif self.mode == 7: + self.setButtons(red = True) + if not sel.navigator: + self.buildAlbumList(addToCache = True) + elif self.mode == 8: + self.setButtons(red = True, green = True, yellow = True) + if not sel.navigator: + self.buildAlbumSongList(albumID = sel.albumID, mode = 7, addToCache = True) + elif self.mode == 10: + self.setButtons(red = True, green = True, yellow = True, blue = True) + if not sel.navigator: + self.buildSongList(addToCache = True) + elif self.mode == 13: + self.setButtons(red = True) + if not sel.navigator: + self.buildGenreList(addToCache = True) + elif self.mode == 14: + self.setButtons(red = True, green = True, yellow = True, blue = True) + if not sel.navigator: + self.buildGenreSongList(genreID = sel.genreID, addToCache = True) + elif self.mode == 18 or self.mode == 19: + if self.mode == 18: + self.setButtons(red = True, green = True, yellow = True) + if self.mode == 19: + self.setButtons(red = True, green = True, blue = True) + if not sel.navigator: + self.red_pressed() # back to main menu --> normally that can not be happened + elif self.mode == 20: + self.setButtons(red = True, green = True, yellow = True, blue = True) + if not sel.navigator: + self.red_pressed() # back to main menu --> normally that can not be happened + + def buildPlaylistList(self, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildPlaylistList, arguments = arguments) + self["headertext"].setText(_("Playlists")) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + playlistList = [] + playlistList.append((Item(text = _("[back]"), mode = 0, navigator = True),)) + cursor.execute("select playlists.playlist_id, playlist_text, count(Playlist_Songs.playlist_id) from playlists left outer join Playlist_Songs on playlists.playlist_id = Playlist_Songs.playlist_id group by playlists.playlist_id order by playlists.playlist_text;") + for row in cursor: + playlistList.append((Item(text = "%s (%d)" % (row[1], row[2]), mode = 2, playlistID = row[0]),)) + cursor.close() + connection.close() + self["list"].setList(playlistList) + if len(playlistList) > 1: + self["list"].moveToIndex(1) + + def buildPlaylistSongList(self, playlistID, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["playlistID"] = playlistID + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildPlaylistSongList, arguments = arguments) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + playlistSongList = [] + playlistSongList.append((Item(text = _("[back]"), mode = 1, navigator = True),)) + cursor.execute("select songs.song_id, title, artists.artist, filename, songs.artist_id, bitrate, length, genre_text, track, date, album_text, songs.Album_id from songs inner join artists on songs.artist_id = artists.artist_id inner join Album on songs.Album_id = Album.Album_id inner join genre on songs.genre_id = genre.genre_id inner join playlist_songs on songs.song_id = playlist_songs.song_id where playlist_songs.playlist_id = %d order by playlist_songs.id;" % (playlistID)) + for row in cursor: + playlistSongList.append((Item(mode = 99, songID = row[0], title = row[1], artist = row[2], filename = row[3], artistID = row[4], bitrate = row[5], length = row[6], genre = row[7], track = row[8], date = row[9], album = row[10], albumID = row[11], playlistID = playlistID),)) + cursor.execute("SELECT playlist_text from playlists where playlist_id = %d;" % playlistID) + row = cursor.fetchone() + self["headertext"].setText(_("Playlist (%s) -> Song List") % row[0]) + cursor.close() + connection.close() + self["list"].setList(playlistSongList) + if len(playlistSongList) > 1: + self["list"].moveToIndex(1) + + def buildGenreList(self, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildGenreList, arguments = arguments) + self["headertext"].setText(_("Genre List")) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + genreList = [] + genreList.append((Item(text = _("[back]"), mode = 0, navigator = True),)) + cursor.execute("select Genre.genre_id,Genre.Genre_text, count(*) from songs inner join Genre on songs.genre_id = Genre.Genre_id group by songs.Genre_id order by Genre.Genre_text;") + for row in cursor: + genreList.append((Item(text = "%s (%d)" % (row[1], row[2]), mode = 14, genreID = row[0]),)) + cursor.close() + connection.close() + self["list"].setList(genreList) + if len(genreList) > 1: + self["list"].moveToIndex(1) + + def buildGenreSongList(self, genreID, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["genreID"] = genreID + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildGenreSongList, arguments = arguments) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + genreSongList = [] + genreSongList.append((Item(text = _("[back]"), mode = 13, navigator = True),)) + cursor.execute("select song_id, title, artists.artist, filename, songs.artist_id, bitrate, length, genre_text, track, date, album_text, songs.Album_id from songs inner join artists on songs.artist_id = artists.artist_id inner join Album on songs.Album_id = Album.Album_id inner join genre on songs.genre_id = genre.genre_id where songs.genre_id = %d order by title, filename;" % (genreID)) + for row in cursor: + genreSongList.append((Item(mode = 99, songID = row[0], title = row[1], artist = row[2], filename = row[3], artistID = row[4], bitrate = row[5], length = row[6], genre = row[7], track = row[8], date = row[9], album = row[10], albumID = row[11], genreID = genreID),)) + cursor.execute("SELECT genre_text from genre where genre_ID = %d;" % genreID) + row = cursor.fetchone() + self["headertext"].setText(_("Genre (%s) -> Song List") % row[0]) + cursor.close() + connection.close() + self["list"].setList(genreSongList) + if len(genreSongList) > 1: + self["list"].moveToIndex(1) + + def setButtons(self, red = False, green = False, yellow = False, blue = False): + if red: + self["key_red"].setText("Main Menu") + else: + self["key_red"].setText("") + if green: + self["key_green"].setText("Play") + else: + self["key_green"].setText("") + if yellow: + self["key_yellow"].setText("All Artists") + else: + self["key_yellow"].setText("") + if blue: + self["key_blue"].setText("Show Album") + else: + self["key_blue"].setText("") + + def info_pressed(self): + if self.player is not None: + if self.player.songList: + self.session.execDialog(self.player) + + def green_pressed(self): + sel = self["list"].l.getCurrentSelection()[0] + if sel is None: + return + if sel.songID != 0: + if self.player is not None: + self.player.doClose() + self.player = None + self.player = self.session.instantiateDialog(MerlinMusicPlayerScreen,self["list"].getList()[1:], self["list"].getCurrentIndex() -1, True) + self.session.execDialog(self.player) + + def red_pressed(self): + self.cacheList = [] + self.setButtons() + self.mode = 0 + self["list"].setMode(self.mode) + self.buildMainMenuList() + + def yellow_pressed(self): + sel = self["list"].l.getCurrentSelection()[0] + if sel.artistID != 0: + oldmode = self.mode + self.mode = 19 + self.setButtons(red = True, green = True, blue = True) + self["list"].setMode(self.mode) + self.buildArtistSongList(artistID = sel.artistID, mode = oldmode, addToCache = True) + + def blue_pressed(self): + sel = self["list"].l.getCurrentSelection()[0] + if sel.albumID != 0: + self.setButtons(red = True, green = True, yellow = True) + oldmode = self.mode + self.mode = 18 + self["list"].setMode(self.mode) + self.buildAlbumSongList(albumID = sel.albumID, mode = oldmode, addToCache = True) + + def buildSongList(self, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildSongList, arguments = arguments) + self["headertext"].setText(_("All Songs")) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + SongList = [] + SongList.append((Item(text = _("[back]"), mode = 0, navigator = True),)) + cursor.execute("select song_id, title, artists.artist, filename, songs.artist_id, bitrate, length, genre_text, track, date, album_text, songs.Album_id from songs inner join artists on songs.artist_id = artists.artist_id inner join Album on songs.Album_id = Album.Album_id inner join genre on songs.genre_id = genre.genre_id order by title, filename;") + for row in cursor: + SongList.append((Item(mode = 99, songID = row[0], title = row[1], artist = row[2], filename = row[3], artistID = row[4], bitrate = row[5], length = row[6], genre = row[7], track = row[8], date = row[9], album = row[10], albumID = row[11]),)) + cursor.close() + connection.close() + self["list"].setList(SongList) + if len(SongList) > 1: + self["list"].moveToIndex(1) + + + def buildSearchSongList(self, sql_where, headerText, mode, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["sql_where"] = sql_where + arguments["headerText"] = headerText + arguments["mode"] = mode + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildSearchSongList, arguments = arguments) + self["headertext"].setText(headerText) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + SongList = [] + SongList.append((Item(text = _("[back]"), mode = mode, navigator = True),)) + cursor.execute("select song_id, title, artists.artist, filename, songs.artist_id, bitrate, length, genre_text, track, date, album_text, songs.Album_id from songs inner join artists on songs.artist_id = artists.artist_id inner join Album on songs.Album_id = Album.Album_id inner join genre on songs.genre_id = genre.genre_id %s order by title, filename;" % sql_where) + for row in cursor: + SongList.append((Item(mode = 99, songID = row[0], title = row[1], artist = row[2], filename = row[3], artistID = row[4], bitrate = row[5], length = row[6], genre = row[7], track = row[8], date = row[9], album = row[10], albumID = row[11]),)) + cursor.close() + connection.close() + self["list"].setList(SongList) + if len(SongList) > 1: + self["list"].moveToIndex(1) + + + def buildArtistSongList(self, artistID, mode, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["artistID"] = artistID + arguments["mode"] = mode + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildArtistSongList, arguments = arguments) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + artistSongList = [] + artistSongList.append((Item(text = _("[back]"), mode = mode, navigator = True),)) + cursor.execute("select song_id, title, artists.artist, filename, bitrate, length, genre_text, track, date, album_text, songs.Album_id from songs inner join artists on songs.artist_id = artists.artist_id inner join Album on songs.Album_id = Album.Album_id inner join genre on songs.genre_id = genre.genre_id where songs.artist_id = %d order by Album.album_text, tracknumber, filename;" % (artistID)) + for row in cursor: + artistSongList.append((Item(mode = 99, songID = row[0], title = row[1], artist = row[2], filename = row[3], bitrate = row[4], length = row[5], genre = row[6], track = row[7], date = row[8], album = row[9], albumID = row[10], artistID = artistID),)) + cursor.execute("SELECT artist from artists where artist_ID = %d;" % artistID) + row = cursor.fetchone() + self["headertext"].setText(_("Artist (%s) -> Song List") % row[0]) + cursor.close() + connection.close() + self["list"].setList(artistSongList) + if len(artistSongList) > 1: + self["list"].moveToIndex(1) + + def buildAlbumSongList(self, albumID, mode, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["albumID"] = albumID + arguments["mode"] = mode + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildAlbumSongList, arguments = arguments) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + albumSongList = [] + albumSongList.append((Item(text = _("[back]"), mode = mode, navigator = True),)) + cursor.execute("select song_id, title, artists.artist, filename, songs.artist_id, bitrate, length, genre_text, track, date, album_text from songs inner join artists on songs.artist_id = artists.artist_id inner join Album on songs.Album_id = Album.Album_id inner join genre on songs.genre_id = genre.genre_id where songs.album_id = %d order by tracknumber, filename;" % (albumID)) + for row in cursor: + albumSongList.append((Item(mode = 99, songID = row[0], title = row[1], artist = row[2], filename = row[3], artistID = row[4], bitrate = row[5], length = row[6], genre = row[7], track = row[8], date = row[9], album = row[10], albumID = albumID),)) + cursor.execute("SELECT album_text from album where album_ID = %d;" % albumID) + row = cursor.fetchone() + self["headertext"].setText(_("Album (%s) -> Song List") % row[0]) + cursor.close() + connection.close() + self["list"].setList(albumSongList) + if len(albumSongList) > 1: + self["list"].moveToIndex(1) + + def buildMainMenuList(self, addToCache = True): + arguments = {} + arguments["addToCache"] = True + self.LastMethod = MethodArguments(method = self.buildMainMenuList, arguments = arguments) + self["headertext"].setText(_("iDream Main Menu")) + mainMenuList = [] + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + # 1. Playlists + cursor.execute("SELECT COUNT (*) FROM playlists;") + row = cursor.fetchone() + mainMenuList.append((Item(text = _("Playlists (%d)") % row[0], mode = 1),)) + # 2. Artists + cursor.execute("SELECT COUNT (*) FROM artists;") + row = cursor.fetchone() + mainMenuList.append((Item(text = _("Artists (%d)") % row[0], mode = 4),)) + # 3. Albums + cursor.execute("SELECT COUNT (DISTINCT album_text) FROM album;") + row = cursor.fetchone() + mainMenuList.append((Item(text = _("Albums (%d)") % row[0], mode = 7),)) + # 4. Songs + cursor.execute("SELECT COUNT (*) FROM songs;") + row = cursor.fetchone() + mainMenuList.append((Item(text = _("Songs (%d)") % row[0], mode = 10),)) + # 5. Genres + cursor.execute("SELECT COUNT (*) FROM genre;") + row = cursor.fetchone() + mainMenuList.append((Item(text = _("Genres (%d)") % row[0], mode = 13),)) + cursor.close() + connection.close() + self["list"].setList(mainMenuList) + self["list"].moveToIndex(0) + + def buildArtistList(self, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildArtistList, arguments = arguments) + self["headertext"].setText(_("Artists List")) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + artistList = [] + artistList.append((Item(text = _("[back]"), mode = 0, navigator = True),)) + cursor.execute("SELECT artists.artist_id,artists.artist, count (distinct album.album_text) FROM songs INNER JOIN artists ON songs.artist_id = artists.artist_id inner join album on songs.album_id = album.album_id GROUP BY songs.artist_id ORDER BY artists.artist;") + for row in cursor: + artistList.append((Item(text = "%s (%d)" % (row[1], row[2]), mode = 5, artistID = row[0]),)) + cursor.close() + connection.close() + self["list"].setList(artistList) + + def buildArtistAlbumList(self, ArtistID, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["ArtistID"] = ArtistID + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildArtistAlbumList, arguments = arguments) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + albumArtistList = [] + albumArtistList.append((Item(text = _("[back]"), mode = 4, navigator = True),)) + cursor.execute("select Album.Album_id,Album.Album_text from songs inner join Album on songs.Album_id = Album.Album_id where songs.artist_id = %d group by songs.Album_id order by Album.Album_text;" % ArtistID) + for row in cursor: + cursor2 = connection.cursor() + cursor2.execute("select count(song_id) from songs where album_id = %d;" % row[0]) + row2 = cursor2.fetchone() + albumArtistList.append((Item(text = "%s (%d)" % (row[1], row2[0]), mode = 6, albumID = row[0], artistID = ArtistID),)) + cursor2.close() + cursor.execute("SELECT artist from artists where artist_ID = %d;" % ArtistID) + row = cursor.fetchone() + self["headertext"].setText(_("Artist (%s) -> Album List") % row[0]) + cursor.close() + connection.close() + self["list"].setList(albumArtistList) + if len(albumArtistList) > 1: + self["list"].moveToIndex(1) + + def buildAlbumList(self, addToCache): + if addToCache: + self.cacheList.append(CacheList(index = self["list"].getCurrentIndex(), listview = self["list"].getList(), headertext = self["headertext"].getText(), methodarguments = self.LastMethod)) + arguments = {} + arguments["addToCache"] = False + self.LastMethod = MethodArguments(method = self.buildAlbumList, arguments = arguments) + self["headertext"].setText(_("Albums List")) + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + albumList = [] + albumList.append((Item(text = _("[back]"), mode = 0, navigator = True),)) + cursor.execute("select Album.Album_id,Album.Album_text, count(*) from songs inner join Album on songs.Album_id = Album.Album_id group by songs.Album_id order by Album.Album_text;") + for row in cursor: + albumList.append((Item(text = "%s (%d)" % (row[1], row[2]), mode = 8, albumID = row[0]),)) + cursor.close() + connection.close() + self["list"].setList(albumList) + if len(albumList) > 1: + self["list"].moveToIndex(1) + + def startRun(self): + if pathToDatabase.isRunning: + self.showScanner = eTimer() + self.showScanner.callback.append(self.showScannerCallback) + self.showScanner.start(0,1) + else: + if config.plugins.merlinmusicplayer.startlastsonglist.value: + self.startPlayerTimer = eTimer() + self.startPlayerTimer.callback.append(self.startPlayerTimerCallback) + self.startPlayerTimer.start(0,1) + self.mode = 0 + self["list"].setMode(self.mode) + self.buildMainMenuList() + + def showScannerCallback(self): + self.session.openWithCallback(self.filesAdded, iDreamAddToDatabase,None) + + + def startPlayerTimerCallback(self): + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + iDreamMode = False + SongList = [] + cursor.execute("select song_id, filename, title, artist, album, genre, bitrate, length, track, date, PTS from CurrentSongList;") + for row in cursor: + SongList.append((Item(songID = row[0], text = os_path.basename(row[1]), filename = row[1], title = row[2], artist = row[3], album = row[4], genre = row[5], bitrate = row[6], length = row[7], track = row[8], date = row[9], PTS = row[10], join = False),)) + if row[0] != 0: + iDreamMode = True + cursor.close() + connection.close() + if self.player is not None: + self.player.doClose() + self.player = None + count = len(SongList) + if count: + # just to be sure, check the index , it's critical + index = config.plugins.merlinmusicplayer.lastsonglistindex.value + if index >= count: + index = 0 + self.player = self.session.instantiateDialog(MerlinMusicPlayerScreen,SongList, index, iDreamMode) + self.session.execDialog(self.player) + + def config(self): + self.session.openWithCallback(self.setupFinished, MerlinMusicPlayerSetup, True) + + def setupFinished(self, result): + if result: + self.red_pressed() + + def Error(self, error = None): + if error is not None: + self["list"].hide() + self["statustext"].setText(str(error.getErrorMessage())) + + def closing(self): + self.close() + + def __onClose(self): + if self.player is not None: + self.player.doClose() + self.player = None + self.session.nav.playService(self.CurrentService) + + def lcdUpdate(self): + try: + count = self["list"].getItemCount() + index = self["list"].getCurrentIndex() + iDreamList = self["list"].getList() + self.summaries.setText(iDreamList[index][0].title or iDreamList[index][0].text,1) + # voheriges + index -= 1 + if index < 0: + index = count + self.summaries.setText(iDreamList[index][0].title or iDreamList[index][0].text,3) + # naechstes + index = self["list"].getCurrentIndex() + 1 + if index > count: + index = 0 + self.summaries.setText(iDreamList[index][0].title or iDreamList[index][0].text,4) + except: pass + + def createSummary(self): + return MerlinMusicPlayerLCDScreenText + + +class iDreamList(GUIComponent, object): + def buildEntry(self, item): + width = self.l.getItemSize().width() + res = [ None ] + if self.displaySongMode: + if item.navigator: + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 3, width , 20, 0, RT_HALIGN_CENTER|RT_VALIGN_CENTER, "%s" % item.text)) + else: + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 3, width - 100 , 20, 0, RT_HALIGN_LEFT|RT_VALIGN_CENTER, "%s - %s" % (item.title, item.artist))) + res.append((eListboxPythonMultiContent.TYPE_TEXT, width - 100,3,100, 20, 1, RT_HALIGN_RIGHT|RT_VALIGN_CENTER, "%s" % item.track)) + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 26,width -200, 18, 1, RT_HALIGN_LEFT|RT_VALIGN_CENTER, "%s%s" % (item.album, item.date))) + res.append((eListboxPythonMultiContent.TYPE_TEXT, width -200, 26,200, 18, 1, RT_HALIGN_RIGHT|RT_VALIGN_CENTER, "%s" % item.length)) + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 47,width -200, 18, 1, RT_HALIGN_LEFT|RT_VALIGN_CENTER, "%s" % item.genre)) + res.append((eListboxPythonMultiContent.TYPE_TEXT, width -200, 47,200, 18, 1, RT_HALIGN_RIGHT|RT_VALIGN_CENTER, "%s" % item.bitrate)) + else: + if item.navigator: + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 3, width , 20, 0, RT_HALIGN_CENTER|RT_VALIGN_CENTER, "%s" % item.text)) + else: + if item.PTS is None: + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 3, width , 20, 0, RT_HALIGN_LEFT|RT_VALIGN_CENTER, "%s" % item.text)) + else: + res.append((eListboxPythonMultiContent.TYPE_TEXT, 0, 3, width , 20, 0, RT_HALIGN_LEFT|RT_VALIGN_CENTER, "%s" % item.title)) + + return res + + def __init__(self): + GUIComponent.__init__(self) + self.l = eListboxPythonMultiContent() + self.l.setBuildFunc(self.buildEntry) + self.l.setFont(0, gFont("Regular", 20)) + self.l.setFont(1, gFont("Regular", 16)) + self.l.setItemHeight(22) + self.onSelectionChanged = [ ] + self.mode = 0 + self.displaySongMode = False + self.list = [] + self.itemCount = 0 + + def connectSelChanged(self, fnc): + if not fnc in self.onSelectionChanged: + self.onSelectionChanged.append(fnc) + + def disconnectSelChanged(self, fnc): + if fnc in self.onSelectionChanged: + self.onSelectionChanged.remove(fnc) + + def selectionChanged(self): + for x in self.onSelectionChanged: + x() + + def getCurrent(self): + cur = self.l.getCurrentSelection() + return cur and cur[0] + + GUI_WIDGET = eListbox + + def postWidgetCreate(self, instance): + instance.setContent(self.l) + instance.selectionChanged.get().append(self.selectionChanged) + + def preWidgetRemove(self, instance): + instance.setContent(None) + instance.selectionChanged.get().remove(self.selectionChanged) + + def moveToIndex(self, index): + self.instance.moveSelectionTo(index) + + def getCurrentIndex(self): + return self.instance.getCurrentIndex() + + currentIndex = property(getCurrentIndex, moveToIndex) + currentSelection = property(getCurrent) + + def setList(self, list): + self.list = list + self.l.setList(list) + self.itemCount = len(self.list) - 1 + + def getItemCount(self): + return self.itemCount + + def getList(self): + return self.list + + def removeItem(self, index): + del self.list[index] + self.l.entryRemoved(index) + + def getDisplaySongMode(self): + return self.displaySongMode + + def setMode(self, mode): + self.mode = mode + if mode == 2 or mode == 6 or mode == 8 or mode == 10 or mode == 18 or mode == 19 or mode == 14 or mode == 20: + self.displaySongMode = True + self.l.setItemHeight(68) + else: + self.displaySongMode = False + self.l.setItemHeight(22) + + +class MerlinMediaPixmap(Pixmap): + def __init__(self): + Pixmap.__init__(self) + self.coverArtFileName = "" + self.picload = ePicLoad() + self.picload.PictureData.get().append(self.paintCoverArtPixmapCB) + self.coverFileNames = ["folder.png", "folder.jpg", "cover.jpg", "cover.png", "coverArt.jpg"] + + def applySkin(self, desktop, screen): + from Tools.LoadPixmap import LoadPixmap + noCoverFile = None + if self.skinAttributes is not None: + for (attrib, value) in self.skinAttributes: + if attrib == "pixmap": + noCoverFile = value + break + if noCoverFile is None: + noCoverFile = resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/no_coverArt.png") + self.noCoverPixmap = LoadPixmap(noCoverFile) + return Pixmap.applySkin(self, desktop, screen) + + def onShow(self): + Pixmap.onShow(self) + sc = AVSwitch().getFramebufferScale() + self.picload.setPara((self.instance.size().width(), self.instance.size().height(), sc[0], sc[1], False, 1, "#00000000")) + + def paintCoverArtPixmapCB(self, picInfo=None): + ptr = self.picload.getData() + if ptr != None: + self.instance.setPixmap(ptr.__deref__()) + + def updateCoverArt(self, path): + back = False + while not path.endswith("/"): + path = path[:-1] + new_coverArtFileName = None + for filename in self.coverFileNames: + if fileExists(path + filename): + new_coverArtFileName = path + filename + if self.coverArtFileName != new_coverArtFileName: + if new_coverArtFileName: + self.coverArtFileName = new_coverArtFileName + print "[MerlinMusicPlayer] using cover from %s " % self.coverArtFileName + self.picload.startDecode(self.coverArtFileName) + back = True + else: + if new_coverArtFileName: + back = True + return back + + def showDefaultCover(self): + self.coverArtFileName = "" + self.instance.setPixmap(self.noCoverPixmap) + + def showCoverFromFile(self, filename): + self.coverArtFileName = filename + self.picload.startDecode(self.coverArtFileName) + + def embeddedCoverArt(self): + print "[embeddedCoverArt] found" + self.coverArtFileName = "/tmp/.id3coverart" + self.picload.startDecode(self.coverArtFileName) + + +class SelectPath(Screen): + skin = """ + + + + + + + + + """ + def __init__(self, session, initDir): + Screen.__init__(self, session) + inhibitDirs = ["/bin", "/boot", "/dev", "/etc", "/lib", "/proc", "/sbin", "/sys", "/usr", "/var"] + inhibitMounts = [] + self["filelist"] = FileList(initDir, showDirectories = True, showFiles = False, inhibitMounts = inhibitMounts, inhibitDirs = inhibitDirs) + self["target"] = Label() + self["actions"] = ActionMap(["WizardActions", "DirectionActions", "ColorActions", "EPGSelectActions"], + { + "back": self.cancel, + "left": self.left, + "right": self.right, + "up": self.up, + "down": self.down, + "ok": self.ok, + "green": self.green, + "red": self.cancel + + }, -1) + self["key_red"] = StaticText(_("Cancel")) + self["key_green"] = StaticText(_("OK")) + + def cancel(self): + self.close(None) + + def green(self): + self.close(self["filelist"].getSelection()[0]) + + def up(self): + self["filelist"].up() + self.updateTarget() + + def down(self): + self["filelist"].down() + self.updateTarget() + + def left(self): + self["filelist"].pageUp() + self.updateTarget() + + def right(self): + self["filelist"].pageDown() + self.updateTarget() + + def ok(self): + if self["filelist"].canDescent(): + self["filelist"].descent() + self.updateTarget() + + def updateTarget(self): + currFolder = self["filelist"].getSelection()[0] + if currFolder is not None: + self["target"].setText(currFolder) + else: + self["target"].setText(_("Invalid Location")) + + +class MerlinMusicPlayerLCDScreen(Screen): + skin = """ + + + Position,ShowHours + + + + """ + + def __init__(self, session, parent): + Screen.__init__(self, session) + self["text1"] = Label() + self["text4"] = Label() + + def setText(self, text, line): + if line == 1: + self["text1"].setText(text) + elif line == 4: + self["text4"].setText(text) + +class MerlinMusicPlayerLCDScreenText(Screen): + skin = """ + + + + + """ + + def __init__(self, session, parent): + Screen.__init__(self, session) + self["text1"] = Label() + self["text3"] = Label() + self["text4"] = Label() + + def setText(self, text, line): + textleer = " " + text = text + textleer*10 + if line == 1: + self["text1"].setText(text) + elif line == 3: + self["text3"].setText(text) + elif line == 4: + self["text4"].setText(text) + + +class MerlinMusicPlayerSetup(Screen, ConfigListScreen): + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + + + + + """ + + elif sz_w == 1024: + skin = """ + + + + + + + """ + else: + skin = """ + + + + + + + """ + + + def __init__(self, session, databasePath): + Screen.__init__(self, session) + + self["key_red"] = StaticText(_("Cancel")) + self["key_green"] = StaticText(_("OK")) + + self.list = [ ] + if HardwareInfo().get_device_name() != "dm7025": + self.list.append(getConfigListEntry(_("Use hardware-decoder for MP3"), config.plugins.merlinmusicplayer.hardwaredecoder)) + self.list.append(getConfigListEntry(_("Play last used songlist after starting"), config.plugins.merlinmusicplayer.startlastsonglist)) + if databasePath: + self.database = getConfigListEntry(_("iDream database path"), config.plugins.merlinmusicplayer.databasepath) + self.list.append(self.database) + else: + self.database = None + self.list.append(getConfigListEntry(_("Use google-images for cover art"), config.plugins.merlinmusicplayer.usegoogleimage)) + self.googleimage = getConfigListEntry(_("Google image path"), config.plugins.merlinmusicplayer.googleimagepath) + self.list.append(self.googleimage) + self.list.append(getConfigListEntry(_("Activate screensaver"), config.plugins.merlinmusicplayer.usescreensaver)) + self.list.append(getConfigListEntry(_("Wait for screensaver (in min)"), config.plugins.merlinmusicplayer.screensaverwait)) + self.list.append(getConfigListEntry(_("Remember last path of filebrowser"), config.plugins.merlinmusicplayer.rememberlastfilebrowserpath)) + self.defaultFileBrowserPath = getConfigListEntry(_("Filebrowser startup path"), config.plugins.merlinmusicplayer.defaultfilebrowserpath) + self.list.append(self.defaultFileBrowserPath) + self.list.append(getConfigListEntry(_("Show iDream in extended-pluginlist"), config.plugins.merlinmusicplayer.idreamextendedpluginlist)) + self.list.append(getConfigListEntry(_("Show Merlin Music Player in extended-pluginlist"), config.plugins.merlinmusicplayer.merlinmusicplayerextendedpluginlist)) + + ConfigListScreen.__init__(self, self.list, session) + self["setupActions"] = ActionMap(["SetupActions", "ColorActions"], + { + "green": self.keySave, + "cancel": self.keyClose, + "ok": self.keySelect, + }, -2) + + def keySelect(self): + cur = self["config"].getCurrent() + if cur == self.database: + self.session.openWithCallback(self.pathSelectedDatabase,SelectPath,config.plugins.merlinmusicplayer.databasepath.value) + elif cur == self.defaultFileBrowserPath: + self.session.openWithCallback(self.pathSelectedFilebrowser,SelectPath,config.plugins.merlinmusicplayer.defaultfilebrowserpath.value) + elif cur == self.googleimage: + self.session.openWithCallback(self.pathSelectedGoogleImage,SelectPath,config.plugins.merlinmusicplayer.googleimagepath.value) + + def pathSelectedGoogleImage(self, res): + if res is not None: + config.plugins.merlinmusicplayer.googleimagepath.value = res + + def pathSelectedDatabase(self, res): + if res is not None: + config.plugins.merlinmusicplayer.databasepath.value = res + + def pathSelectedFilebrowser(self, res): + if res is not None: + config.plugins.merlinmusicplayer.defaultfilebrowserpath.value = res + + def keySave(self): + for x in self["config"].list: + x[1].save() + configfile.save() + self.close(True) + + def keyClose(self): + for x in self["config"].list: + x[1].cancel() + self.close(False) + + + +class MerlinMusicPlayerFileList(Screen): + + sz_w = getDesktop(0).size().width() + if sz_w == 1280: + skin = """ + + + + + + + + """ + elif sz_w == 1024: + skin = """ + + + + + + + """ + else: + skin = """ + + + + + + + + """ + + + def __init__(self, session): + self.session = session + Screen.__init__(self, session) + self["list"] = FileList(config.plugins.merlinmusicplayer.defaultfilebrowserpath.value, showDirectories = True, showFiles = True, matchingPattern = "(?i)^.*\.(mp3|m4a|flac|ogg|m3u|pls|cue)", useServiceRef = False) + + + self["actions"] = ActionMap(["WizardActions", "DirectionActions", "ColorActions", "EPGSelectActions"], + { + "ok": self.ok, + "back": self.close, + "input_date_time": self.config, + "info" : self.info_pressed, + }, -1) + + self["headertext"] = Label(_("Filelist")) + self.player = None + self.onClose.append(self.__onClose) + self.onLayoutFinish.append(self.startRun) + self.CurrentService = self.session.nav.getCurrentlyPlayingServiceReference() + self.session.nav.stopService() + + def startRun(self): + if config.plugins.merlinmusicplayer.startlastsonglist.value: + self.startPlayerTimer = eTimer() + self.startPlayerTimer.callback.append(self.startPlayerTimerCallback) + self.startPlayerTimer.start(0,1) + + def startPlayerTimerCallback(self): + connection = OpenDatabase() + connection.text_factory = str + cursor = connection.cursor() + iDreamMode = False + SongList = [] + cursor.execute("select song_id, filename, title, artist, album, genre, bitrate, length, track, date, PTS from CurrentSongList;") + for row in cursor: + SongList.append((Item(songID = row[0], text = os_path.basename(row[1]), filename = row[1], title = row[2], artist = row[3], album = row[4], genre = row[5], bitrate = row[6], length = row[7], track = row[8], date = row[9], PTS = row[10], join = False),)) + if row[0] != 0: + iDreamMode = True + cursor.close() + connection.close() + if self.player is not None: + self.player.doClose() + self.player = None + count = len(SongList) + if count: + # just to be sure, check the index , it's critical + index = config.plugins.merlinmusicplayer.lastsonglistindex.value + if index >= count: + index = 0 + self.player = self.session.instantiateDialog(MerlinMusicPlayerScreen,SongList, index, iDreamMode) + self.session.execDialog(self.player) + + def readCUE(self, filename): + SongList = [] + displayname = None + try: + cuefile = open(filename, "r") + except IOError: + return None + import re + performer_re = re.compile(r"""PERFORMER "(?P.*?)"(?:=\r\n|\r|\n|$)""") + title_re = re.compile(r"""TITLE "(?P.*?)"(?:=\r\n|\r|\n|$)""") + filename_re = re.compile(r"""FILE "(?P<filename>.+?)".*(?:=\r\n|\r|\n|$)""", re.DOTALL) + track_re = re.compile(r"""TRACK (?P<track_number>[^ ]+?)(?:[ ]+.*?)(?:=\r\n|\r|\n|$)""") + index_re = re.compile(r"""INDEX (?P<index_nr>[^ ]+?)[ ]+(?P<track_index>[^ ]+?)(?:=\r\n|\r|\n|$)""") + msts_re = re.compile("""^(?P<mins>[0-9]{1,}):(?P<secs>[0-9]{2}):(?P<ms>[0-9]{2})$""") + songfilename = "" + album = "" + performer = "" + title = "" + pts = 0 + state = 0 # header + for line in cuefile.readlines(): + entry = line.strip() + m = filename_re.search(entry) + if m: + if m.group('filename')[0] == "/": + songfilename = m.group('filename') + else: + songfilename = os_path.dirname(filename) + "/" + m.group('filename') + m = title_re.search(entry) + if m: + if state == 0: + album = m.group('title') + else: + title = m.group('title') + m = performer_re.search(entry) + if m: + performer = m.group('performer') + m = track_re.search(entry) + if m: + state = 1 # tracks + m = index_re.search(entry) + if m: + if int(m.group('index_nr')) == 1: + m1 = msts_re.search(m.group('track_index')) + if m1: + pts = (int(m1.group('mins')) * 60 + int(m1.group('secs'))) * 90000 + SongList.append((Item(text = title, filename = songfilename, title = title, artist = performer, album = album,join = False, PTS = pts),)) + cuefile.close() + return SongList + + def readM3U(self, filename): + SongList = [] + displayname = None + try: + m3ufile = open(filename, "r") + except IOError: + return None + for line in m3ufile.readlines(): + entry = line.strip() + if entry != "": + if entry.startswith("#EXTINF:"): + extinf = entry.split(',',1) + if len(extinf) > 1: + displayname = extinf[1] + elif entry[0] != "#": + if entry[0] == "/": + songfilename = entry + else: + songfilename = os_path.dirname(filename) + "/" + entry + if displayname: + text = displayname + displayname = None + else: + text = entry + SongList.append((Item(text = text, filename = songfilename),)) + m3ufile.close() + return SongList + + def readPLS(self, filename): + SongList = [] + displayname = None + try: + plsfile = open(filename, "r") + except IOError: + return None + entry = plsfile.readline().strip() + if entry == "[playlist]": + while True: + entry = plsfile.readline().strip() + if entry == "": + break + if entry[0:4] == "File": + pos = entry.find('=') + 1 + newentry = entry[pos:] + SongList.append((Item(text = newentry, filename = newentry),)) + else: + SongList = self.readM3U(filename) + plsfile.close() + return SongList + + def ok(self): + if self["list"].canDescent(): + self["list"].descent() + else: + SongList = [] + index = 0 + foundIndex = 0 + count = 0 + currentFilename = self["list"].getFilename() + if currentFilename.lower().endswith(".m3u"): + SongList = self.readM3U(os_path.join(self["list"].getCurrentDirectory(),currentFilename)) + elif currentFilename.lower().endswith(".pls"): + SongList = self.readPLS(os_path.join(self["list"].getCurrentDirectory(),currentFilename)) + elif currentFilename.lower().endswith(".cue"): + SongList = self.readCUE(os_path.join(self["list"].getCurrentDirectory(),currentFilename)) + else: + files = os_listdir(self["list"].getCurrentDirectory()) + files.sort() + for filename in files: + if filename.lower().endswith(".mp3") or filename.lower().endswith(".flac") or filename.lower().endswith(".m4a") or filename.lower().endswith(".ogg"): + SongList.append((Item(text = filename, filename = os_path.join(self["list"].getCurrentDirectory(),filename)),)) + if self["list"].getFilename() == filename: + foundIndex = index + index += 1 + if self.player is not None: + self.player.doClose() + self.player = None + count = len(SongList) + if count: + self.player = self.session.instantiateDialog(MerlinMusicPlayerScreen,SongList, foundIndex, False) + self.session.execDialog(self.player) + else: + self.session.open(MessageBox, _("No music files found!"), type = MessageBox.TYPE_INFO,timeout = 20 ) + + def config(self): + self.session.open(MerlinMusicPlayerSetup, True) + + def info_pressed(self): + if self.player is not None: + if self.player.songList: + self.session.execDialog(self.player) + + def __onClose(self): + if self.player is not None: + self.player.doClose() + self.player = None + self.session.nav.playService(self.CurrentService) + if config.plugins.merlinmusicplayer.rememberlastfilebrowserpath.value: + config.plugins.merlinmusicplayer.defaultfilebrowserpath.value = self["list"].getCurrentDirectory() + config.plugins.merlinmusicplayer.defaultfilebrowserpath.save() + + +def main(session,**kwargs): + session.open(iDreamMerlin) + +def merlinmusicplayerfilelist(session,**kwargs): + session.open(MerlinMusicPlayerFileList) + +def Plugins(**kwargs): + + list = [PluginDescriptor(name="Merlin iDream", description=_("Dreambox Music Database"), where = [PluginDescriptor.WHERE_PLUGINMENU], icon = "iDream.png", fnc=main)] + list.append(PluginDescriptor(name="Merlin Music Player", description=_("Merlin Music Player"), where = [PluginDescriptor.WHERE_PLUGINMENU], icon = "MerlinMusicPlayer.png", fnc=merlinmusicplayerfilelist)) + if config.plugins.merlinmusicplayer.idreamextendedpluginlist.value: + list.append(PluginDescriptor(name="iDream", description=_("Dreambox Music Database"), where = [PluginDescriptor.WHERE_EXTENSIONSMENU], fnc=main)) + if config.plugins.merlinmusicplayer.merlinmusicplayerextendedpluginlist.value: + list.append(PluginDescriptor(name="Merlin Music Player", description=_("Merlin Music Player"), where = [PluginDescriptor.WHERE_EXTENSIONSMENU], fnc=merlinmusicplayerfilelist)) + return list + -- 2.7.4