Merge lp:~mblayman/entertainer/clean-messages into lp:entertainer

Proposed by Matt Layman
Status: Rejected
Rejected by: Paul Hummer
Proposed branch: lp:~mblayman/entertainer/clean-messages
Merge into: lp:entertainer
Diff against target: None lines
To merge this branch: bzr merge lp:~mblayman/entertainer/clean-messages
Reviewer Review Type Date Requested Status
Paul Hummer Needs Resubmitting
Review via email: mp+11338@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Matt Layman (mblayman) wrote :

Dependent on working-sdist.

This is just some work to clean out a bunch of junk in the backend and try to separate the client from the backend in a more clear manner.

I did a bunch of analysis of what we are/aren't using in terms of messages. This led to some nice cleanup and removal of some methods that simply weren't needed.

I know this may not seem like important work, but it means that there will be less pain when it comes time to completely remove elements of the backend that exist in the client code. Less contact points means less work in the future.

Revision history for this message
Paul Hummer (rockstar) wrote :

Please re-propose this against future, as that's where the rest of the dependent branches will land.

review: Needs Resubmitting

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANIFEST.in'
2--- MANIFEST.in 2008-10-05 17:11:31 +0000
3+++ MANIFEST.in 2009-09-05 13:58:02 +0000
4@@ -1,3 +1,10 @@
5 include entertainer*
6+include docs/COPYING docs/DEPENDENCIES
7+include docs/entertainer.1 docs/entertainer.desktop
8 recursive-include cfg *
9+recursive-include entertainerlib *
10+recursive-include icons *
11+recursive-include themes *
12+recursive-include tools *
13+global-exclude *pyc
14
15
16=== added file 'README'
17--- README 1970-01-01 00:00:00 +0000
18+++ README 2009-09-05 13:58:02 +0000
19@@ -0,0 +1,6 @@
20+To install Entertainer, run:
21+
22+python setup.py install
23+
24+For information on how to contribute, read docs/HACKING.
25+
26
27=== modified file 'cfg/content.conf'
28--- cfg/content.conf 2009-05-31 17:11:16 +0000
29+++ cfg/content.conf 2009-08-16 22:19:22 +0000
30@@ -1,26 +1,34 @@
31 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
32-[Images]
33-folders =
34-display_hidden_files_folders = False
35+[Media]
36+download_lyrics = False
37+download_album_art = True
38+download_metadata = True
39+display_eject_in_menu = False
40+folders =
41
42 [Weather]
43 location = Bath,England
44 display_in_menu = True
45-metric_units = True
46-
47-[Music]
48-folders =
49-download_lyrics = False
50-download_album_art = True
51-
52-[Videos]
53-folders =
54-download_metadata = True
55
56 [RSS]
57-feeds = http://theironlion.net/feeds/blog;http://www.joshuascotton.com/main/archives/tag/entertainer/feed;http://laymanstermsdev.wordpress.com/feed
58+feeds = http://theironlion.net/blog/feed;http://www.joshuascotton.com/main/archives/tag/entertainer/feed;http://laymanstermsdev.wordpress.com/feed
59 fetch_interval = 15
60
61 [CD]
62 display_eject_in_menu = False
63
64+[Photographs]
65+slideshow_step = 5
66+
67+[General]
68+stage_width = 1366
69+stage_height = 768
70+show_effects = True
71+start_in_fullscreen = True
72+theme = Default
73+backend_port = 45054
74+history_size = 8
75+transition_effect = Slide
76+start_server_auto = True
77+display_icon = False
78+
79
80=== removed file 'cfg/preferences.conf'
81--- cfg/preferences.conf 2009-05-06 03:40:22 +0000
82+++ cfg/preferences.conf 1970-01-01 00:00:00 +0000
83@@ -1,16 +0,0 @@
84-# Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
85-[Photographs]
86-slideshow_step = 5
87-
88-[General]
89-stage_width = 1366
90-stage_height = 768
91-show_effects = True
92-start_in_fullscreen = True
93-theme = Default
94-backend_port = 45054
95-history_size = 8
96-transition_effect = Slide
97-start_server_auto = True
98-display_icon = False
99-
100
101=== modified file 'docs/DEPENDENCIES'
102--- docs/DEPENDENCIES 2009-05-09 19:08:39 +0000
103+++ docs/DEPENDENCIES 2009-08-25 02:51:36 +0000
104@@ -14,3 +14,4 @@
105 python-pyvorbis
106 python-storm
107 python-twisted
108+python-xdg
109
110=== modified file 'entertainer'
111--- entertainer 2009-05-06 02:58:08 +0000
112+++ entertainer 2009-05-10 17:36:49 +0000
113@@ -1,8 +1,9 @@
114 #!/usr/bin/env python
115 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
116+'''Main client executable'''
117 '''Main frontend executable'''
118
119-from entertainerlib.frontend import main
120+from entertainerlib.client import main
121
122 if __name__ == '__main__':
123 main()
124
125=== modified file 'entertainer-backend'
126--- entertainer-backend 2009-05-06 02:58:08 +0000
127+++ entertainer-backend 2009-09-08 02:02:58 +0000
128@@ -51,11 +51,7 @@
129 sys.exit(0)
130
131 if len(sys.argv) > 1 and sys.argv[1] == "--foreground":
132- try:
133- backend = BackendServer()
134- except KeyboardInterrupt:
135- backend.quitBackend()
136- sys.exit()
137+ backend = BackendServer()
138 else:
139 print "Entertainer backend starting..."
140 libc = ctypes.CDLL('libc.so.6')
141
142=== renamed file 'entertainer-content-manager' => 'entertainer-manager'
143--- entertainer-content-manager 2009-04-28 23:54:49 +0000
144+++ entertainer-manager 2009-08-23 01:01:18 +0000
145@@ -1,15 +1,14 @@
146 #!/usr/bin/env python
147 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
148-'''Content Management executable'''
149+'''Manager executable'''
150
151 import gtk
152
153-from entertainerlib.frontend.translation_setup import TranslationSetup
154+from entertainerlib.client.translation_setup import TranslationSetup
155 TranslationSetup()
156
157-from entertainerlib.utils.content_management_dialog import (
158- ContentManagementDialog)
159-
160-
161-ContentManagementDialog(True)
162+from entertainerlib.dialog import ManagerDialog
163+
164+
165+ManagerDialog(True)
166 gtk.main()
167
168=== removed file 'entertainer-preferences'
169--- entertainer-preferences 2009-04-28 23:54:49 +0000
170+++ entertainer-preferences 1970-01-01 00:00:00 +0000
171@@ -1,14 +0,0 @@
172-#!/usr/bin/env python
173-# Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
174-'''A preferences editing tool'''
175-
176-import gtk
177-
178-from entertainerlib.frontend.translation_setup import TranslationSetup
179-TranslationSetup()
180-
181-from entertainerlib.utils.preferences_dialog import PreferencesDialog
182-
183-
184-PreferencesDialog(True)
185-gtk.main()
186
187=== added file 'entertainer-server'
188--- entertainer-server 1970-01-01 00:00:00 +0000
189+++ entertainer-server 2009-04-04 04:24:46 +0000
190@@ -0,0 +1,7 @@
191+#!/usr/bin/env python
192+'''Server executable'''
193+
194+from entertainerlib.network import server_main
195+
196+server_main()
197+
198
199=== modified file 'entertainerlib/backend/backend_server.py'
200--- entertainerlib/backend/backend_server.py 2009-05-06 02:58:08 +0000
201+++ entertainerlib/backend/backend_server.py 2009-09-08 02:02:58 +0000
202@@ -3,8 +3,8 @@
203
204 import gobject
205
206-from entertainerlib.utils.configuration import Configuration
207-from entertainerlib.utils.logger import Logger
208+from entertainerlib.configuration import Configuration
209+from entertainerlib.logger import Logger
210
211 # Entertainer backend core
212 from entertainerlib.backend.core.message import Message
213@@ -39,18 +39,13 @@
214 self.config = Configuration()
215 self.logger = Logger().getLogger('backend.BackendServer')
216 self.message_bus = MessageBus()
217- self._port = self.config.get_port()
218+ self._port = self.config.port
219
220 # Connection server - Thread that listens incoming socket connections
221 self.connection_server = None
222
223- # Config files
224- self.content_config = None
225- self.preferences = None
226-
227 self.scheduler = None
228 self.feed_manager = None
229- self.guide_updater = None
230 self.media_manager = None
231
232 # The order of the initialize method calls is significant! Don't change
233@@ -65,15 +60,13 @@
234 """Initialize configuration"""
235 cfg_dict = {
236 MessageType.CONTENT_CONF_UPDATED : MessagePriority.VERY_HIGH,
237- MessageType.PREFERENCES_CONF_UPDATED : MessagePriority.VERY_HIGH
238 }
239 self.message_bus.registerMessageHandler(self.config, cfg_dict)
240 self.logger.debug("Configuration intialized successfully")
241
242 def initialize_connection_server(self):
243 """Initialize connection server."""
244- self.connection_server = ConnectionServer(self._port,
245- self.message_bus)
246+ self.connection_server = ConnectionServer(self._port, self.message_bus)
247 # Start listening incoming connections
248 self.connection_server.start()
249
250@@ -81,7 +74,7 @@
251 """Initialize message scheduler."""
252 self.scheduler = MessageScheduler(self.message_bus)
253 self.scheduler.addMessage(Message(MessageType.UPDATE_FEEDS),
254- 60 * self.config.get_feed_fetch_interval())
255+ 60 * self.config.feed_fetch_interval)
256 self.logger.debug("Message scheduler intialized successfully")
257
258 def initialize_feed_manager(self):
259@@ -105,7 +98,3 @@
260 self.message_bus.registerMessageHandler(self.media_manager, media_dict)
261 self.logger.debug("Media Manager intialized successfully")
262
263- def quitBackend(self):
264- '''Close the backend server'''
265- self.message_bus.unregisterAllMessageHandlers()
266-
267
268=== modified file 'entertainerlib/backend/components/feeds/feed_fetcher.py'
269--- entertainerlib/backend/components/feeds/feed_fetcher.py 2009-05-06 02:58:08 +0000
270+++ entertainerlib/backend/components/feeds/feed_fetcher.py 2009-05-10 17:36:49 +0000
271@@ -7,8 +7,8 @@
272 from datetime import datetime
273 from pysqlite2 import dbapi2 as sqlite
274
275-from entertainerlib.utils.configuration import Configuration
276-from entertainerlib.utils.logger import Logger
277+from entertainerlib.configuration import Configuration
278+from entertainerlib.logger import Logger
279
280 # Messaging system
281 from entertainerlib.backend.core.message import Message
282
283=== modified file 'entertainerlib/backend/components/feeds/feed_manager.py'
284--- entertainerlib/backend/components/feeds/feed_manager.py 2009-05-06 02:58:08 +0000
285+++ entertainerlib/backend/components/feeds/feed_manager.py 2009-08-16 23:01:49 +0000
286@@ -5,8 +5,8 @@
287 from pysqlite2 import dbapi2 as sqlite
288 from entertainerlib.backend.components.feeds.feed_fetcher import FeedFetcher
289
290-from entertainerlib.utils.configuration import Configuration
291-from entertainerlib.utils.logger import Logger
292+from entertainerlib.configuration import Configuration
293+from entertainerlib.logger import Logger
294
295 # Messaging system
296 from entertainerlib.backend.core.message_type_priority import MessageType
297@@ -35,8 +35,7 @@
298 Update all feeds to cache in a new thread and after
299 that emit FEED_DB_UPDATED message to the messagebus.
300 """
301- fetch_thread = FeedFetcher(self.message_bus,
302- self.config.get_feeds())
303+ fetch_thread = FeedFetcher(self.message_bus, self.config.feeds)
304 fetch_thread.start()
305
306 def createFeedCacheDatabase(self):
307
308=== renamed file 'entertainerlib/utils/feed_utils.py' => 'entertainerlib/backend/components/feeds/feed_utils.py'
309--- entertainerlib/utils/feed_utils.py 2009-05-06 02:02:20 +0000
310+++ entertainerlib/backend/components/feeds/feed_utils.py 2009-05-10 17:36:49 +0000
311@@ -8,7 +8,7 @@
312 import gtk
313 import gtk.glade
314
315-from entertainerlib.utils.configuration import Configuration
316+from entertainerlib.configuration import Configuration
317
318
319 class FeedEntryParser:
320
321=== modified file 'entertainerlib/backend/components/mediacache/image_cache.py'
322--- entertainerlib/backend/components/mediacache/image_cache.py 2009-06-01 09:55:40 +0000
323+++ entertainerlib/backend/components/mediacache/image_cache.py 2009-08-16 18:51:06 +0000
324@@ -8,8 +8,8 @@
325 from pysqlite2 import dbapi2 as sqlite
326
327 from entertainerlib.thumbnailer import ImageThumbnailer
328-from entertainerlib.utils.configuration import Configuration
329-from entertainerlib.utils.logger import Logger
330+from entertainerlib.configuration import Configuration
331+from entertainerlib.logger import Logger
332
333 from entertainerlib.backend.components.mediacache.cache import Cache
334
335@@ -117,15 +117,13 @@
336 "Path doesn't exist: " + path)
337 else:
338 for root, dirs, files in os.walk(path):
339- if os.path.split(root)[-1][0] == "." and not \
340- self.config.display_hidden_files_folders():
341+ if os.path.split(root)[-1][0] == ".":
342 continue
343 if not self.isDirectoryInCache(root):
344 self._addAlbum(root)
345
346 for name in files:
347- if os.path.split(name)[-1][0] == "." and not \
348- self.config.display_hidden_files_folders():
349+ if os.path.split(name)[-1][0] == ".":
350 continue
351 if self.isSupportedFormat(name):
352 self.addFile(os.path.join(root, name))
353
354=== modified file 'entertainerlib/backend/components/mediacache/media_cache_manager.py'
355--- entertainerlib/backend/components/mediacache/media_cache_manager.py 2009-05-06 02:58:08 +0000
356+++ entertainerlib/backend/components/mediacache/media_cache_manager.py 2009-08-16 22:19:22 +0000
357@@ -1,8 +1,8 @@
358 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
359 '''MediaCacheManager - Downloads metadata and keeps media cache up-to-date'''
360
361-from entertainerlib.utils.configuration import Configuration
362-from entertainerlib.utils.logger import Logger
363+from entertainerlib.configuration import Configuration
364+from entertainerlib.logger import Logger
365
366 from entertainerlib.backend.core.message_type_priority import MessageType
367 from entertainerlib.backend.core.message_handler import MessageHandler
368@@ -13,7 +13,7 @@
369 from entertainerlib.backend.components.mediacache.video_cache import VideoCache
370
371 class MediaCacheManager(MessageHandler):
372- """Makes sure that frontend has all the data available."""
373+ """Makes sure that client has all the data available."""
374
375 def __init__(self):
376 """
377@@ -23,11 +23,11 @@
378 self.logger = Logger().getLogger(
379 'backend.components.mediacache.MediaCacheManager')
380 self.config = Configuration()
381- self.video_folders = self.config.get_video_folders()
382+ self.video_folders = self.config.media_folders
383 self._index_videos(self.video_folders)
384- self.music_folders = self.config.get_music_folders()
385+ self.music_folders = self.config.media_folders
386 self._index_music(self.music_folders)
387- self.image_folders = self.config.get_image_folders()
388+ self.image_folders = self.config.media_folders
389 self._index_images(self.image_folders)
390
391 # Should we rebuild to detect files that were removed while backend was
392@@ -104,9 +104,9 @@
393 we need to index them. If folders are removed, we need to remove
394 them from the cache and also from FileSystemObeserver.
395 """
396- updated_video_folders = self.config.get_video_folders()
397- updated_music_folders = self.config.get_music_folders()
398- updated_image_folders = self.config.get_image_folders()
399+ updated_video_folders = self.config.media_folders
400+ updated_music_folders = self.config.media_folders
401+ updated_image_folders = self.config.media_folders
402
403 # Handle image folder changes
404 current_images = set(self.image_folders)
405
406=== modified file 'entertainerlib/backend/components/mediacache/music_cache.py'
407--- entertainerlib/backend/components/mediacache/music_cache.py 2009-05-09 17:03:51 +0000
408+++ entertainerlib/backend/components/mediacache/music_cache.py 2009-08-18 02:50:40 +0000
409@@ -9,11 +9,11 @@
410 import ogg.vorbis
411 from pysqlite2 import dbapi2 as sqlite
412
413-from entertainerlib.utils.albumart_downloader import AlbumArtDownloader
414-from entertainerlib.utils.configuration import Configuration
415-from entertainerlib.utils.logger import Logger
416-
417 from entertainerlib.backend.components.mediacache.cache import Cache
418+from entertainerlib.configuration import Configuration
419+from entertainerlib.download import AlbumArtDownloader
420+from entertainerlib.logger import Logger
421+
422
423 class MusicCache(Cache):
424 """
425@@ -433,7 +433,7 @@
426 album_art_file)
427 # Local not found -> try internet
428 else:
429- if self.config.download_album_art():
430+ if self.config.download_album_art:
431 if album != "Unknown album" and artist != "Unknown Artist":
432 loader_thread = AlbumArtDownloader(album, artist,
433 self.config.ALBUM_ART_DIR)
434
435=== modified file 'entertainerlib/backend/components/mediacache/video_cache.py'
436--- entertainerlib/backend/components/mediacache/video_cache.py 2009-05-06 02:58:08 +0000
437+++ entertainerlib/backend/components/mediacache/video_cache.py 2009-08-18 02:45:02 +0000
438@@ -6,8 +6,8 @@
439 from pysqlite2 import dbapi2 as sqlite
440
441 from entertainerlib.thumbnailer import VideoThumbnailer
442-from entertainerlib.utils.configuration import Configuration
443-from entertainerlib.utils.logger import Logger
444+from entertainerlib.configuration import Configuration
445+from entertainerlib.logger import Logger
446
447 from entertainerlib.backend.components.mediacache.cache import Cache
448 from entertainerlib.backend.components.mediacache.video_metadata_search import (
449@@ -243,7 +243,7 @@
450 VALUES (:fn)""",
451 { "fn" : filename } )
452 self.__db_conn.commit()
453- if self.config.download_video_metadata():
454+ if self.config.download_metadata:
455 self.__searchMetadata(filename)
456
457 def __searchMetadata(self, filename):
458
459=== modified file 'entertainerlib/backend/components/mediacache/video_metadata_search.py'
460--- entertainerlib/backend/components/mediacache/video_metadata_search.py 2009-05-06 02:58:08 +0000
461+++ entertainerlib/backend/components/mediacache/video_metadata_search.py 2009-05-10 17:36:49 +0000
462@@ -9,8 +9,8 @@
463 import threading
464 from pysqlite2 import dbapi2 as sqlite
465
466-from entertainerlib.utils.logger import Logger
467-from entertainerlib.utils.configuration import Configuration
468+from entertainerlib.logger import Logger
469+from entertainerlib.configuration import Configuration
470
471 class VideoMetadataSearch(threading.Thread):
472 """
473
474=== modified file 'entertainerlib/backend/core/client_connection.py'
475--- entertainerlib/backend/core/client_connection.py 2009-05-06 02:58:08 +0000
476+++ entertainerlib/backend/core/client_connection.py 2009-05-10 17:36:49 +0000
477@@ -8,7 +8,7 @@
478 # Messaging system
479 from entertainerlib.backend.core.message_handler import MessageHandler
480
481-from entertainerlib.utils.logger import Logger
482+from entertainerlib.logger import Logger
483
484 class ClientConnection(threading.Thread, MessageHandler):
485 """
486
487=== modified file 'entertainerlib/backend/core/connection_server.py'
488--- entertainerlib/backend/core/connection_server.py 2009-05-06 02:58:08 +0000
489+++ entertainerlib/backend/core/connection_server.py 2009-05-10 17:36:49 +0000
490@@ -7,7 +7,7 @@
491
492 from entertainerlib.backend.core.client_connection import ClientConnection
493
494-from entertainerlib.utils.logger import Logger
495+from entertainerlib.logger import Logger
496
497 class ConnectionServer(threading.Thread):
498 """
499
500=== modified file 'entertainerlib/backend/core/message_bus.py'
501--- entertainerlib/backend/core/message_bus.py 2009-05-06 02:58:08 +0000
502+++ entertainerlib/backend/core/message_bus.py 2009-09-08 02:02:58 +0000
503@@ -6,7 +6,7 @@
504 from entertainerlib.backend.core.message import Message
505 from entertainerlib.backend.core.message_handler import MessageHandler
506 from entertainerlib.backend.core.message_type_priority import MessageType
507-from entertainerlib.utils.logger import Logger
508+from entertainerlib.logger import Logger
509
510 class MessageBus:
511 """
512@@ -23,12 +23,7 @@
513 When MessageHandler is registered to the MessageBus there is also another
514 parameter besides handler itself. Second parameter is a dictionary that
515 defines MessageTypes that registered handler wants to be notified of and
516- also priorities for those message types.
517-
518- Example of second parameter:
519- dict = {MessageType.FRONTEND_OPENED : MessagePriority.LOW ,
520- MessageType.FRONTEND_CLOSED : MessagePriority.NORMAL }
521- """
522+ also priorities for those message types."""
523
524 # This determines number of message types avaialble. In other words, this
525 # variable tells how many variables is defined in MessageType class.
526@@ -89,12 +84,6 @@
527 self.logger.debug("MessageHandler '" + str(message_handler) +
528 "' unregistered from the message bus.")
529
530- def unregisterAllMessageHandlers(self):
531- """
532- Unregisters all MessageHandler from this MessageBus.
533- """
534- self.message_handlers[:] = []
535-
536 def notifyMessage(self, message):
537 """
538 Emit a new Message to this MessageBus.
539
540=== modified file 'entertainerlib/backend/core/message_bus_proxy.py'
541--- entertainerlib/backend/core/message_bus_proxy.py 2009-05-06 02:58:08 +0000
542+++ entertainerlib/backend/core/message_bus_proxy.py 2009-08-16 22:03:14 +0000
543@@ -6,7 +6,7 @@
544 import threading
545 from cStringIO import StringIO
546
547-from entertainerlib.utils.configuration import Configuration
548+from entertainerlib.configuration import Configuration
549
550 class MessageBusProxy(threading.Thread):
551 """
552@@ -57,7 +57,7 @@
553 MessageHandler.
554 """
555 # Open socket
556- self.socket_to_server.connect(('localhost', self.config.get_port()))
557+ self.socket_to_server.connect(('localhost', self.config.port))
558
559 # Send client name
560 self.socket_to_server.sendall(self.client_name + "\n")
561
562=== modified file 'entertainerlib/backend/core/message_type_priority.py'
563--- entertainerlib/backend/core/message_type_priority.py 2009-05-06 02:58:08 +0000
564+++ entertainerlib/backend/core/message_type_priority.py 2009-09-08 00:53:03 +0000
565@@ -15,56 +15,27 @@
566 VERY_LOW = 40
567
568 class MessageType:
569- """
570- Determines all allowed Message types. MessageHandler should use these
571- to determine type. This simply makes code more readable.
572-
573- Example:
574- if message.get_type() == MessageType.FRONTEND_OPENED:
575- do_something_useful()
576- """
577-
578- # Indicates that Preferences UI has been used to update preferences.
579- PREFERENCES_CONF_UPDATED = 0
580+ """Determines all allowed Message types. MessageHandler should use these to
581+ determine type. This simply makes code more readable."""
582
583 # Indicates that Content Management UI has been used to update contents.
584- CONTENT_CONF_UPDATED = 1
585-
586- # Indicates that frontend has been opened.
587- FRONTEND_OPENED = 2
588-
589- # Indicates that frontend has been closed.
590- FRONTEND_CLOSED = 3
591+ CONTENT_CONF_UPDATED = 0
592
593 # Indicates that Feed cache has been updated.
594- FEED_DB_UPDATED = 4
595-
596- NOT_USED_5 = 5
597-
598- NOT_USED_1 = 6
599+ FEED_DB_UPDATED = 1
600
601 # Indicates that Feed cache should be updated.
602- UPDATE_FEEDS = 7
603-
604- NOT_USED_6 = 8
605-
606- #This should be left in otherwise message_bus.py breaks
607- #If another message type is needed please use this
608- DO_NOT_DELETE_USE_NEXT = 9
609-
610- NOT_USED_2 = 10
611- NOT_USED_3 = 11
612- NOT_USED_4 = 12
613+ UPDATE_FEEDS = 2
614
615 # Require to rebuild image cache
616- REBUILD_IMAGE_CACHE = 13
617+ REBUILD_IMAGE_CACHE = 3
618
619 # Require to rebuild music cache
620- REBUILD_MUSIC_CACHE = 14
621+ REBUILD_MUSIC_CACHE = 4
622
623 # Require to rebuild video cache
624- REBUILD_VIDEO_CACHE = 15
625+ REBUILD_VIDEO_CACHE = 5
626
627 # Require to rebuild feed cache
628- REBUILD_FEED_CACHE = 16
629+ REBUILD_FEED_CACHE = 6
630
631
632=== renamed directory 'entertainerlib/frontend' => 'entertainerlib/client'
633=== modified file 'entertainerlib/client/__init__.py'
634--- entertainerlib/frontend/__init__.py 2009-05-06 03:40:22 +0000
635+++ entertainerlib/client/__init__.py 2009-08-21 01:55:09 +0000
636@@ -1,12 +1,12 @@
637 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
638-'''Frontend gui to entertainer'''
639+'''Client code for Entertainer.'''
640 # pylint: disable-msg=W0612
641
642 def main(*args, **kwargs):
643- '''Frontend runner'''
644+ '''Client code main loop.'''
645
646 # Import statements are inside the function so that they aren't imported
647- # every time something from the frontend is imported
648+ # every time something from the client is imported
649
650 # cluttergtk must be imported before the first import of clutter so it
651 # must be imported even though pylint complains about it not being used.
652@@ -15,23 +15,23 @@
653 import gobject
654 import gtk
655
656- from entertainerlib.frontend.translation_setup import TranslationSetup
657+ from entertainerlib.client.translation_setup import TranslationSetup
658 TranslationSetup()
659
660 from entertainerlib.backend.backend_server import BackendServer
661- from entertainerlib.utils.configuration import Configuration
662- from entertainerlib.frontend.frontend_client import FrontendClient
663+ from entertainerlib.configuration import Configuration
664+ from entertainerlib.client.client import Client
665
666 gobject.threads_init()
667 gtk.gdk.threads_init()
668 clutter.threads_init()
669
670 config = Configuration()
671- if config.start_auto_server():
672+ if config.start_auto_server:
673 print "Entertainer backend starting..."
674 BackendServer()
675
676- frontend_client = FrontendClient()
677- frontend_client.start()
678+ client_client = Client()
679+ client_client.start()
680
681
682
683=== modified file 'entertainerlib/client/backend_connection.py'
684--- entertainerlib/frontend/backend_connection.py 2009-05-06 02:58:08 +0000
685+++ entertainerlib/client/backend_connection.py 2009-09-08 02:40:16 +0000
686@@ -7,49 +7,21 @@
687 from entertainerlib.backend.core.message_handler import MessageHandler
688
689 class BackendConnection(MessageHandler):
690- """
691- BackendConnection - Connection to the Entertainer backend. Instance from
692- this class is a gate to backend messagebus.
693- """
694+ """Connection to the Entertainer backend messagebus."""
695
696 def __init__(self):
697- """Initialize connection. Connects to backend."""
698- #messages = {
699- # MessageType.CONTENT_CONF_UPDATED : MessagePriority.NORMAL,
700- # MessageType.PREFERENCES_CONF_UPDATED : MessagePriority.NORMAL,
701- # MessageType.FEED_DB_UPDATED : MessagePriority.HIGH }
702- # XXX: rockstar - The messages above are the "real" messages, and I'm
703- # not sure why they are commented out. Anyone?
704 MessageHandler.__init__(self)
705 messages = {}
706 name = "Entertainer Frontend"
707 self.message_bus_proxy = MessageBusProxy(messages, self, name)
708 self.message_bus_proxy.connectToMessageBus()
709 self.message_bus_proxy.start()
710- self.message_bus_proxy.sendMessage(
711- Message(MessageType.FRONTEND_OPENED))
712
713 def close_connection(self):
714 """Close connection to backend"""
715- self.message_bus_proxy.sendMessage(
716- Message(MessageType.FRONTEND_CLOSED))
717 self.message_bus_proxy.disconnectFromMessageBus()
718
719 def request_feed_update(self):
720 """Request backend to fetch all feeds from the Internet."""
721 self.message_bus_proxy.sendMessage(Message(MessageType.UPDATE_FEEDS))
722
723- def request_weather_update(self):
724- """Request backend to update weather information from the Internet."""
725- self.message_bus_proxy.sendMessage(Message(MessageType.UPDATE_WEATHER))
726-
727- # Implements MessageHandler interface
728- def handleMessage(self, message):
729- """Handle received messages. (Implements MessageHandler interface)"""
730- if message.get_type() == MessageType.CONTENT_CONF_UPDATED:
731- pass
732- elif message.get_type() == MessageType.PREFERENCES_CONF_UPDATED:
733- pass
734- elif message.get_type() == MessageType.FEED_DB_UPDATED:
735- pass
736-
737
738=== renamed file 'entertainerlib/frontend/frontend_client.py' => 'entertainerlib/client/client.py'
739--- entertainerlib/frontend/frontend_client.py 2009-05-06 02:58:08 +0000
740+++ entertainerlib/client/client.py 2009-09-08 02:40:16 +0000
741@@ -1,71 +1,65 @@
742 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
743-'''Entertainer Frontend - This is a client side of Entertainer.'''
744+'''Entertainer client.'''
745
746 import sys
747
748 import gtk
749-
750-from entertainerlib.frontend.backend_connection import BackendConnection
751-from entertainerlib.frontend.gui.user_interface import UserInterface
752-from entertainerlib.frontend.medialibrary.feeds import FeedLibrary
753-from entertainerlib.frontend.medialibrary.music import MusicLibrary
754-from entertainerlib.frontend.medialibrary.images import ImageLibrary
755-from entertainerlib.frontend.medialibrary.videos import VideoLibrary
756-from entertainerlib.utils.configuration import Configuration
757-from entertainerlib.utils.logger import Logger
758-from entertainerlib.utils.system_tray_icon import SystemTrayIcon
759-
760-class FrontendClient:
761- '''
762- Entertainer frontend
763-
764- This is a frontend application of the Entertainer. Frontend is a GUI part
765- that user sees on the screen. This class is a core of the frontend. Frontend
766- connects to the backend's messagebus at startup.
767- '''
768+from twisted.internet import gtk2reactor
769+gtk2reactor.install() # Install the gtk2 reactor before import the real reactor
770+from twisted.internet import reactor
771+from twisted.internet.protocol import ClientCreator
772+from twisted.python.log import startLogging
773+
774+from entertainerlib.client.backend_connection import BackendConnection
775+from entertainerlib.client.medialibrary.feeds import FeedLibrary
776+from entertainerlib.client.medialibrary.music import MusicLibrary
777+from entertainerlib.client.medialibrary.images import ImageLibrary
778+from entertainerlib.client.medialibrary.videos import VideoLibrary
779+from entertainerlib.configuration import Configuration
780+from entertainerlib.gui.user_interface import UserInterface
781+from entertainerlib.gui.system_tray_icon import SystemTrayIcon
782+from entertainerlib.network.local.client import EntertainerLocalClientProtocol
783+
784+
785+class Client:
786+ '''This is a client application of the Entertainer. Entertainer's client
787+ hooks into the server, and then provides a user interface for the data the
788+ server creates.'''
789
790 def __init__(self):
791- '''
792- Create a new frontend.
793-
794- This initializes all frontend stuff like media librarys, remote
795- control receiver and GUI. After this we just wait user actions.
796- '''
797 config = Configuration()
798- self.logger = Logger().getLogger('frontend.FrontendClient')
799- self.backend_connection = self.initialize_backend_connection()
800+ self.backend_connection = BackendConnection()
801 feed_library = FeedLibrary(self.backend_connection)
802- music_library = MusicLibrary(self.backend_connection)
803- image_library = ImageLibrary(self.backend_connection)
804- video_library = VideoLibrary(self.backend_connection)
805+ music_library = MusicLibrary()
806+ image_library = ImageLibrary()
807+ video_library = VideoLibrary()
808 self.ui = UserInterface(
809 feed_library, image_library, music_library, video_library,
810- self.quit_frontend)
811-
812- if config.tray_icon_enabled():
813- SystemTrayIcon(
814- self.quit_frontend, self.toggle_interface_visibility)
815+ self.quit_client)
816+
817+ if config.tray_icon_enabled:
818+ SystemTrayIcon(self.quit_client, self.toggle_interface_visibility)
819+
820+ startLogging(sys.stdout)
821+ client = EntertainerLocalClientProtocol
822+
823+ ClientCreator(reactor, client).connectTCP(
824+ config.network_options['host'],
825+ config.network_options['port'])
826
827 def start(self):
828 '''Start the necessary main loop.'''
829 self.ui.start_up()
830 self.interface_visible = True
831 gtk.gdk.threads_enter()
832- gtk.main()
833+ reactor.run()
834 gtk.gdk.threads_leave()
835
836- def initialize_backend_connection(self):
837- '''Connect to the backend server.'''
838- backend_connection = BackendConnection()
839- self.logger.debug('Connected to the Entertainer backend server.')
840-
841- return backend_connection
842-
843- def quit_frontend(self):
844- '''Clean up the connection to the backend then close the frontend.'''
845+ def quit_client(self):
846+ '''Clean up the connection to the backend then close the client.'''
847 self.backend_connection.close_connection()
848
849- gtk.main_quit()
850+ reactor.stop()
851 sys.exit(0)
852
853 def toggle_interface_visibility(self):
854
855=== modified file 'entertainerlib/client/media_player.py'
856--- entertainerlib/frontend/media_player.py 2009-06-29 19:41:35 +0000
857+++ entertainerlib/client/media_player.py 2009-06-30 01:06:01 +0000
858@@ -9,10 +9,10 @@
859 import gobject
860 import gst
861
862-from entertainerlib.frontend.gui.widgets.motion_buffer import MotionBuffer
863-from entertainerlib.frontend.gui.widgets.texture import Texture
864-from entertainerlib.frontend.medialibrary.playable import Playable
865-from entertainerlib.utils.logger import Logger
866+from entertainerlib.gui.widgets.motion_buffer import MotionBuffer
867+from entertainerlib.gui.widgets.texture import Texture
868+from entertainerlib.client.medialibrary.playable import Playable
869+from entertainerlib.logger import Logger
870
871 class MediaPlayer(gobject.GObject, object):
872 '''
873@@ -72,7 +72,7 @@
874 self.is_playing = False # Is media player currently playing
875 self.is_reactive_allowed = False # Is the video_texture reactive
876
877- self.logger = Logger().getLogger('frontend.MediaPlayer')
878+ self.logger = Logger().getLogger('client.MediaPlayer')
879
880 self._internal_callback_timeout_key = None
881
882
883=== modified file 'entertainerlib/client/medialibrary/feeds.py'
884--- entertainerlib/frontend/medialibrary/feeds.py 2009-06-25 19:49:48 +0000
885+++ entertainerlib/client/medialibrary/feeds.py 2009-07-29 03:09:34 +0000
886@@ -3,7 +3,7 @@
887
888 from pysqlite2 import dbapi2 as sqlite
889
890-from entertainerlib.utils.configuration import Configuration
891+from entertainerlib.configuration import Configuration
892
893 class FeedLibrary(object):
894 '''This library can be used to handle RSS feeds in Entertainer.'''
895
896=== modified file 'entertainerlib/client/medialibrary/images.py'
897--- entertainerlib/frontend/medialibrary/images.py 2009-05-06 03:40:22 +0000
898+++ entertainerlib/client/medialibrary/images.py 2009-09-08 02:40:16 +0000
899@@ -4,25 +4,16 @@
900 import os
901 from pysqlite2 import dbapi2 as sqlite
902
903-from entertainerlib.utils.configuration import Configuration
904+from entertainerlib.configuration import Configuration
905
906 class ImageLibrary:
907- """
908- Image library.
909-
910- Entertainer's image cache.
911- """
912-
913- def __init__(self, backend_connection):
914- """
915- Initialize image library
916- @param backend_connection: BackendConnection object
917- """
918+ """Entertainer's image cache."""
919+
920+ def __init__(self):
921 self.config = Configuration()
922
923 if not os.path.exists(self.config.IMAGE_DB):
924 raise Exception("Image database doesn't exist!")
925- self.backend_connection = backend_connection
926
927 def get_all_images(self):
928 """
929
930=== modified file 'entertainerlib/client/medialibrary/music.py'
931--- entertainerlib/frontend/medialibrary/music.py 2009-07-19 19:27:31 +0000
932+++ entertainerlib/client/medialibrary/music.py 2009-09-08 02:40:16 +0000
933@@ -5,10 +5,10 @@
934 import CDDB, DiscID
935 from pysqlite2 import dbapi2 as sqlite
936
937-from entertainerlib.utils.configuration import Configuration
938+from entertainerlib.configuration import Configuration
939
940-from entertainerlib.frontend.medialibrary.playable import Playable
941-from entertainerlib.utils.lyrics_downloader import LyricsDownloader
942+from entertainerlib.client.medialibrary.playable import Playable
943+from entertainerlib.download import LyricsDownloader
944
945
946 class MusicLibraryException(Exception):
947@@ -28,22 +28,13 @@
948 pass
949
950 class MusicLibrary:
951- """
952- Music library.
953-
954- Interface for Entertainer's music cache.
955- """
956-
957- def __init__(self, backend_connection):
958- """
959- Initialize library.
960- @param backend_connection: BackendConnection object
961- """
962+ """Interface for Entertainer's music cache."""
963+
964+ def __init__(self):
965 self.config = Configuration()
966
967 if not os.path.exists(self.config.MUSIC_DB):
968 raise Exception("Music database doesn't exist!")
969- self.backend_connection = backend_connection
970 self.db_connection = sqlite.connect(self.config.MUSIC_DB)
971 self.cursor = self.db_connection.cursor()
972
973
974=== modified file 'entertainerlib/client/medialibrary/videos.py'
975--- entertainerlib/frontend/medialibrary/videos.py 2009-05-06 02:58:08 +0000
976+++ entertainerlib/client/medialibrary/videos.py 2009-09-08 02:40:16 +0000
977@@ -4,28 +4,19 @@
978 import os
979 from pysqlite2 import dbapi2 as sqlite
980
981-from entertainerlib.utils.configuration import Configuration
982-from entertainerlib.utils.logger import Logger
983+from entertainerlib.configuration import Configuration
984+from entertainerlib.logger import Logger
985
986-from entertainerlib.frontend.medialibrary.playable import Playable
987+from entertainerlib.client.medialibrary.playable import Playable
988
989 class VideoLibrary:
990- """
991- Video library.
992-
993- Interface for Entertainer's video cache.
994- """
995-
996- def __init__(self, backend_connection):
997- """
998- Initialize video library
999- @param backend_connection: BackendConnection object
1000- """
1001+ """Interface for Entertainer's video cache."""
1002+
1003+ def __init__(self):
1004 self.config = Configuration()
1005
1006 if not os.path.exists(self.config.VIDEO_DB):
1007 raise Exception("Video database doesn't exist!")
1008- self.backend_connection = backend_connection
1009
1010 def get_movies(self):
1011 """
1012@@ -133,7 +124,7 @@
1013
1014 #Setting default values
1015 self.logger = Logger().getLogger(
1016- 'frontend.medialibrary.videos.VideoItem')
1017+ 'client.medialibrary.videos.VideoItem')
1018 self.__title = ""
1019 self.__filename = ""
1020 self.__length = 0
1021@@ -220,12 +211,12 @@
1022 else:
1023 self.logger.error("Thumbnail does not exist for " + \
1024 self.get_filename() + ", using default art instead")
1025- return os.path.join(self.config.get_theme_path(),
1026+ return os.path.join(self.config.theme_path,
1027 "images/default_movie_art.png")
1028 else:
1029 self.logger.error("Thumbnail does not exist for " + \
1030 self.get_filename() + ", using default art instead")
1031- return os.path.join(self.config.get_theme_path(),
1032+ return os.path.join(self.config.theme_path,
1033 "images/default_movie_art.png")
1034
1035 def has_thumbnail(self):
1036
1037=== modified file 'entertainerlib/client/translation_setup.py'
1038--- entertainerlib/frontend/translation_setup.py 2009-05-06 02:02:20 +0000
1039+++ entertainerlib/client/translation_setup.py 2009-08-28 02:38:48 +0000
1040@@ -1,4 +1,3 @@
1041-#!/usr/bin/env python
1042 # Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
1043 '''Translation Setup Code'''
1044
1045@@ -7,33 +6,37 @@
1046 import gettext
1047 import gtk
1048 import gtk.glade
1049-
1050-TRANSLATION_SOURCE = {
1051- 'branch' : os.path.abspath(os.path.dirname(__file__) + '/../../locale'),
1052- # Hardcoded path for a package install
1053- 'package' : "/usr/share/locale"
1054- }
1055+from xdg import BaseDirectory
1056
1057 class TranslationSetup:
1058 def __init__(self):
1059+ '''Because of how early translation setup has to occur. This should be
1060+ the only file outside of Configuration that imports xdg.'''
1061+
1062+ def install_locale(locale_dir):
1063+ '''Install locale data from the provided directory.'''
1064+ # This sets up the _ function
1065+ gettext.install('entertainer', locale_dir)
1066+
1067+ # This sets up the glade translations
1068+ gtk.glade.bindtextdomain('entertainer', locale_dir)
1069+
1070 # Find locale data from a dev branch if we can
1071- if os.path.exists(TRANSLATION_SOURCE['branch']):
1072- #This setups the _ function
1073- gettext.install('entertainer', TRANSLATION_SOURCE['branch'])
1074-
1075- #This setups the glade translations
1076- gtk.glade.bindtextdomain('entertainer',
1077- TRANSLATION_SOURCE['branch'])
1078- # Install locale data from the hardcoded package path
1079- elif os.path.exists(TRANSLATION_SOURCE['package']):
1080- #This setups the _ function
1081- gettext.install('entertainer', TRANSLATION_SOURCE['package'])
1082-
1083- #This setups the glade translations
1084- gtk.glade.bindtextdomain('entertainer',
1085- TRANSLATION_SOURCE['package'])
1086+ dev_locale = os.path.abspath(os.path.dirname(__file__) +
1087+ '/../../locale')
1088+ if os.path.exists(dev_locale):
1089+ install_locale(dev_locale)
1090+
1091+ # Install locale data from the system location
1092 else:
1093- pass
1094+ system_data_dirs = [data_dir for data_dir in
1095+ BaseDirectory.xdg_data_dirs if not
1096+ data_dir.startswith(BaseDirectory.xdg_data_home)]
1097+ # Since we don't know for certain where the mo files were installed,
1098+ # we try to install from both /usr/share and /usr/local/share.
1099+ for data_dir in system_data_dirs:
1100+ system_locale = os.path.join(data_dir, 'locale')
1101+ install_locale(system_locale)
1102
1103- gtk.glade.textdomain ('entertainer')
1104+ gtk.glade.textdomain('entertainer')
1105
1106
1107=== renamed file 'entertainerlib/utils/configuration.py' => 'entertainerlib/configuration.py'
1108--- entertainerlib/utils/configuration.py 2009-06-01 09:55:40 +0000
1109+++ entertainerlib/configuration.py 2009-08-27 03:18:41 +0000
1110@@ -2,475 +2,217 @@
1111 '''Configuration - Class represents Entertainer's configuration'''
1112
1113 import os
1114-import sys
1115 import shutil
1116 import ConfigParser
1117-from ConfigParser import ParsingError, NoSectionError, NoOptionError
1118-
1119+from ConfigParser import NoSectionError, NoOptionError
1120+
1121+from xdg import BaseDirectory
1122+
1123+from entertainerlib.backend.core.message_handler import MessageHandler
1124 from entertainerlib.backend.core.message_type_priority import MessageType
1125-from entertainerlib.backend.core.message_handler import MessageHandler
1126-
1127-from entertainerlib.utils.theme import Theme
1128-
1129-SOURCE_CONFIG = {
1130- 'branch' : os.path.abspath(os.path.dirname(__file__) + '/../../cfg'),
1131- # Hardcoded path for a package install
1132- 'package' : "/usr/share/entertainer/cfg"
1133- }
1134+from entertainerlib.db.connection import Database
1135+from entertainerlib.gui.theme import Theme
1136
1137 class Configuration(MessageHandler):
1138- """
1139- Configuration of Entertainer
1140-
1141- This class is an interface to all configuration parameters. All components
1142- of Entertainer should get configuration values through this class. Object
1143- from this class reads current config files and returns values based on
1144- those files. If there are missing lines or other errors in config file,
1145- then object return default values defined in this class.
1146- """
1147-
1148- CFG_DIR = os.path.expanduser("~/.config/entertainer")
1149- TEST_DIR = None
1150-
1151- # This dictionary keeps track of data values that are written to config
1152- # files but need to be returned to a known state while testing.
1153- _tainted = {}
1154-
1155- # This dictionary keeps track of data values that are tainted in memory
1156- # instead of a config file
1157- _tainted_in_memory = {}
1158+ '''Interface to all configuration parameters. All components of Entertainer
1159+ should get configuration values through this class.'''
1160
1161 _shared_state = {}
1162
1163 def __init__(self, test_dir=None):
1164- """
1165- Read configuration files and setup this object.
1166- """
1167 self.__dict__ = self._shared_state
1168 MessageHandler.__init__(self)
1169
1170- if not self._shared_state:
1171- self.TEST_DIR = test_dir
1172-
1173+ if not self._shared_state or test_dir is not None:
1174 # Set in a production mode or a test mode
1175- if self.TEST_DIR is None:
1176- self.cfg_dir = self.CFG_DIR
1177+ if test_dir is None:
1178+ self.resources = Resources()
1179 else:
1180- self.cfg_dir = self.TEST_DIR
1181-
1182- if not os.path.exists(os.path.expanduser(self.cfg_dir)):
1183- self.create_cfg_dir()
1184-
1185- self.ENTERTAINER_LOG = os.path.join(self.cfg_dir,
1186- u'entertainer.log')
1187-
1188- self.FEED_DB = os.path.join(self.cfg_dir, 'cache/feed.db')
1189- self.IMAGE_DB = os.path.join(self.cfg_dir, 'cache/image.db')
1190- self.MUSIC_DB = os.path.join(self.cfg_dir, 'cache/music.db')
1191- self.VIDEO_DB = os.path.join(self.cfg_dir, 'cache/video.db')
1192-
1193- self.THUMB_DIR = os.path.join(self.cfg_dir, 'cache', 'thumbnails')
1194+ self.resources = Resources(config_testing_dir=test_dir)
1195+
1196+ self.cache_dir = self.resources.cache_dir
1197+ self.config_dir = self.resources.config_dir
1198+ self.data_dir = self.resources.data_dir
1199+
1200+ self.LOG = os.path.join(self.cache_dir, u'entertainer.log')
1201+
1202+ self.MEDIA_DB = Database(os.path.join(self.cache_dir, 'media'))
1203+
1204+ self.FEED_DB = os.path.join(self.cache_dir, 'feed.db')
1205+ self.IMAGE_DB = os.path.join(self.cache_dir, 'image.db')
1206+ self.MUSIC_DB = os.path.join(self.cache_dir, 'music.db')
1207+ self.VIDEO_DB = os.path.join(self.cache_dir, 'video.db')
1208+
1209+ self.THUMB_DIR = os.path.join(self.cache_dir, 'thumbnails')
1210 self.IMAGE_THUMB_DIR = os.path.join(self.THUMB_DIR, 'image')
1211 self.VIDEO_THUMB_DIR = os.path.join(self.THUMB_DIR, 'video')
1212- self.ALBUM_ART_DIR = os.path.join(self.cfg_dir, 'cache/album_art')
1213- self.MOVIE_ART_DIR = os.path.join(self.cfg_dir, 'cache/movie_art')
1214-
1215- # Preferences file
1216- self.preferences_conf = os.path.join(self.cfg_dir,
1217- 'preferences.conf')
1218-
1219- # Content file
1220- self.content_conf = os.path.join(self.cfg_dir, 'content.conf')
1221-
1222- self.content_config = ConfigParser.ConfigParser()
1223- self.preferences = ConfigParser.ConfigParser()
1224- try:
1225- self.content_config.readfp(open(self.content_conf))
1226- self.preferences.readfp(open(self.preferences_conf))
1227- except ParsingError:
1228- print("ParsingError with configuration file.")
1229- sys.exit(1)
1230- except IOError:
1231- print("IOError: Couldn't read configuration file.")
1232- sys.exit(1)
1233-
1234- self.theme = Theme(self.get_theme_path())
1235-
1236- self.stage_width = None
1237- self.stage_height = None
1238-
1239- def create_cfg_dir(self):
1240- '''Create a configuration directory and default config files.'''
1241-
1242- def create_cache_hierarchy(config_dir):
1243- '''Create the directories needed for the cache'''
1244- directories = [
1245- 'cache',
1246- 'cache/album_art',
1247- 'cache/movie_art',
1248- 'cache/thumbnails',
1249- 'cache/thumbnails/image',
1250- 'cache/thumbnails/video'
1251- ]
1252-
1253- for directory in directories:
1254- new_dir = os.path.join(config_dir, directory)
1255- os.mkdir(new_dir)
1256-
1257- config_dir = self.get_cfg_dir()
1258- try:
1259- # Copy configuration data from a dev branch if we can
1260- if os.path.exists(SOURCE_CONFIG['branch']):
1261- shutil.copytree(SOURCE_CONFIG['branch'], config_dir)
1262- create_cache_hierarchy(config_dir)
1263- # Install configuration data from the hardcoded package path
1264- elif os.path.exists(SOURCE_CONFIG['package']):
1265- shutil.copytree(SOURCE_CONFIG['package'], config_dir)
1266- create_cache_hierarchy(config_dir)
1267- else:
1268- print "Couldn't find configuration data. Execution aborted."
1269- sys.exit(1)
1270- except OSError:
1271- print "Couldn't copy configuration data to %s. Execution aborted." \
1272- % config_dir
1273- sys.exit(1)
1274+ self.ALBUM_ART_DIR = os.path.join(self.cache_dir, 'album_art')
1275+ self.MOVIE_ART_DIR = os.path.join(self.cache_dir, 'movie_art')
1276+
1277+ self.read_config_file()
1278+
1279+ self.theme = Theme(self.theme_path)
1280+
1281+ self._stage_width = None
1282+ self._stage_height = None
1283+
1284+ # Network options specify the server type and extra options
1285+ self.network_options = {
1286+ 'type': 'local',
1287+ 'host': 'localhost',
1288+ 'port': 55545}
1289+
1290+ def read_config_file(self):
1291+ '''Read in the config file.'''
1292+ self.content_conf = os.path.join(self.config_dir, 'content.conf')
1293+ self.content = ConfigParser.ConfigParser()
1294+ self.content.readfp(open(self.content_conf))
1295
1296 def update_configuration(self):
1297- """
1298- Read configuration files again and update this object.
1299- """
1300- try:
1301- self.content_config.readfp(open(self.content_conf))
1302- self.preferences.readfp(open(self.preferences_conf))
1303- except ParsingError:
1304- raise Exception("Syntax error in configuration file.")
1305- except IOError:
1306- raise IOError("Couldn't read config file.")
1307+ '''Read configuration file again and update this object.'''
1308+ self.content.readfp(open(self.content_conf))
1309
1310- def write_content_value(self, section, option, value, sanitize=False):
1311+ def write_content_value(self, section, option, value):
1312 """Write a new value to the content configuration file."""
1313- if self.TEST_DIR and not sanitize:
1314- self.taint('content', section, option)
1315
1316- try:
1317- self.content_config.set(section, option, value)
1318+ def write_value(section, option, value):
1319+ '''Actually write the value to the section and option.'''
1320+ self.content.set(section, option, value)
1321 cfg_file = file(self.content_conf, 'w')
1322- self.content_config.write(cfg_file)
1323+ self.content.write(cfg_file)
1324+
1325+ try:
1326+ write_value(section, option, value)
1327 except NoSectionError:
1328- raise Exception("No Section to set in content.conf file")
1329+ # Provide an upgrade path to additions of new sections.
1330+ shutil.rmtree(self.config_dir)
1331+ self.resources.create_configuration()
1332+ self.read_config_file()
1333+ write_value(section, option, value)
1334 except NoOptionError:
1335 raise Exception("No Option to set in content.conf file")
1336
1337- def write_preference_value(self, section, option, value, sanitize=False):
1338- """Write a new value to the preferences configuration file."""
1339- if self.TEST_DIR and not sanitize:
1340- self.taint('preferences', section, option)
1341-
1342- try:
1343- self.preferences.set(section, option, value)
1344- cfg_file = file(self.preferences_conf, 'w')
1345- self.preferences.write(cfg_file)
1346- except NoSectionError:
1347- raise Exception("No Section to set in preferences.conf file")
1348- except NoOptionError:
1349- raise Exception("No Option to set in preferences.conf file")
1350-
1351- def get_stage_width(self):
1352- '''Get the stage width from the preferences'''
1353- if self.stage_width:
1354- return self.stage_width
1355-
1356- try:
1357- self.stage_width = self.preferences.getint("General", "stage_width")
1358- except (NoSectionError, NoOptionError):
1359- self.stage_width = 1366 # Default
1360-
1361- return self.stage_width
1362-
1363- def set_stage_width(self, new_width, sanitize=False):
1364- '''Set the stage width for the in memory instance'''
1365- # Taint data if in test mode
1366- if self.TEST_DIR and not sanitize:
1367- kwargs = {'new_width' : self.get_stage_width(),
1368- 'sanitize' : True}
1369- self.taint_in_memory(self.set_stage_width, kwargs)
1370-
1371- self.stage_width = new_width
1372-
1373- def get_stage_height(self):
1374- '''Get the stage height from the preferences'''
1375- if self.stage_height:
1376- return self.stage_height
1377-
1378- try:
1379- self.stage_height = self.preferences.getint("General",
1380- "stage_height")
1381- except (NoSectionError, NoOptionError):
1382- self.stage_height = 768 # Default
1383-
1384- return self.stage_height
1385-
1386- def set_stage_height(self, new_height, sanitize=False):
1387- '''Set the stage height for the in memory instance'''
1388- # Taint data if in test mode
1389- if self.TEST_DIR and not sanitize:
1390- kwargs = {'new_height' : self.get_stage_height(),
1391- 'sanitize' : True}
1392- self.taint_in_memory(self.set_stage_height, kwargs)
1393-
1394- self.stage_height = new_height
1395-
1396- def get_theme_path(self):
1397- """
1398- Get absolute path of the used theme.
1399- @return: string (path)
1400- """
1401- theme_path = os.path.join(self.get_cfg_dir(), 'themes')
1402- try:
1403- theme = self.preferences.get("General", "theme")
1404- except (NoSectionError, NoOptionError):
1405- return os.path.join(theme_path, "Default")
1406-
1407- if os.path.exists(os.path.join(theme_path, theme)):
1408- return os.path.join(theme_path, theme)
1409- else:
1410- return os.path.join(theme_path, "Default")
1411-
1412+ def _get_stage_width(self):
1413+ '''stage_width property getter.'''
1414+ if self._stage_width:
1415+ return self._stage_width
1416+
1417+ self._stage_width = self.content.getint("General", "stage_width")
1418+ return self._stage_width
1419+
1420+ def _set_stage_width(self, new_width):
1421+ '''stage_width property setter.'''
1422+ self._stage_width = new_width
1423+
1424+ stage_width = property(_get_stage_width, _set_stage_width)
1425+
1426+ def _get_stage_height(self):
1427+ '''stage_height property getter.'''
1428+ if self._stage_height:
1429+ return self._stage_height
1430+
1431+ self._stage_height = self.content.getint("General", "stage_height")
1432+ return self._stage_height
1433+
1434+ def _set_stage_height(self, new_height):
1435+ '''stage_height property setter.'''
1436+ self._stage_height = new_height
1437+
1438+ stage_height = property(_get_stage_height, _set_stage_height)
1439+
1440+ @property
1441+ def theme_path(self):
1442+ '''Path to the currently selected theme.'''
1443+ theme_path = os.path.join(self.data_dir, 'themes')
1444+ theme = self.content.get("General", "theme")
1445+ return os.path.join(theme_path, theme)
1446+
1447+ @property
1448 def tray_icon_enabled(self):
1449- """
1450- Display tray icon.
1451- @return: boolean
1452- """
1453- try:
1454- result = self.preferences.getboolean("General", "display_icon")
1455- except (NoSectionError, NoOptionError):
1456- return False
1457- return result
1458-
1459- def get_feed_fetch_interval(self):
1460- """
1461- Get time interval (in minutes) for feed fetching.
1462- @return: Integer
1463- """
1464- try:
1465- result = self.content_config.getint("RSS", "fetch_interval")
1466- except (NoSectionError, NoOptionError):
1467- return 60 # Default is one hour
1468- return result
1469-
1470- def get_port(self):
1471- """
1472- Get backend server's port number.
1473- @return: Integer
1474- """
1475- try:
1476- result = self.preferences.getint("General", "backend_port")
1477- except (NoSectionError, NoOptionError):
1478- return 45054 # Default port
1479- return result
1480-
1481- def get_video_folders(self):
1482- """
1483- Get list of video folders
1484- @return:String Array
1485- """
1486- try:
1487- video_list = self.content_config.get("Videos", "folders")
1488- result = self._is_valid_media_folder(video_list.split(';'))
1489- except (NoSectionError, NoOptionError):
1490- return []
1491- return result
1492-
1493- def get_music_folders(self):
1494- """
1495- Get list of music folders
1496- @return: String Array
1497- """
1498- try:
1499- music_list = self.content_config.get("Music", "folders")
1500- result = music_list.split(';')
1501- except (NoSectionError, NoOptionError):
1502- return []
1503- return result
1504-
1505- def get_image_folders(self):
1506- """
1507- Get list of image folders
1508- @return: String Array
1509- """
1510- try:
1511- image_list = self.content_config.get("Images", "folders")
1512- result = self._is_valid_media_folder(image_list.split(';'))
1513- except (NoSectionError, NoOptionError):
1514- return []
1515- return result
1516-
1517- def get_feeds(self):
1518- """
1519- Get list of feeds.
1520- @return: String Array
1521- """
1522- try:
1523- rss_feeds = self.content_config.get("RSS", "feeds")
1524- result = rss_feeds.split(';')
1525- except (NoSectionError, NoOptionError):
1526- return []
1527- return result
1528-
1529- def get_weather_location(self):
1530- """
1531- Get list of weather location codes.
1532- @return: List of strings
1533- """
1534- try:
1535- location = self.content_config.get("Weather", "location")
1536- except (NoSectionError, NoOptionError):
1537- location = ''
1538- return location
1539-
1540- def display_weather_in_frontend(self):
1541- """
1542- Should we display weather in frontend
1543- @return: Boolean
1544- """
1545- try:
1546- result = self.content_config.getboolean("Weather",
1547- "display_in_menu")
1548- except (NoSectionError, NoOptionError):
1549- return False
1550- return result
1551-
1552- def display_cd_eject_in_frontend(self):
1553- """
1554- Should we display the cd eject button in frontend
1555- @return: Boolean
1556- """
1557- try:
1558- result = self.content_config.getboolean("CD",
1559- "display_eject_in_menu")
1560- except (NoSectionError, NoOptionError):
1561- return False
1562- return result
1563-
1564- def download_video_metadata(self):
1565- """
1566- Get True if video metadata should be downloaded, otherwise False
1567- """
1568- try:
1569- result = self.content_config.getboolean("Videos",
1570- "download_metadata")
1571- except (NoSectionError, NoOptionError):
1572- return False
1573- return result
1574-
1575+ '''Boolean to indicate whether or not to show the tray icon.'''
1576+ return self.content.getboolean("General", "display_icon")
1577+
1578+ @property
1579+ def feed_fetch_interval(self):
1580+ '''Time interval (in minutes) for feed fetching.'''
1581+ return self.content.getint("RSS", "fetch_interval")
1582+
1583+ @property
1584+ def port(self):
1585+ '''Server's port number.'''
1586+ return self.content.getint("General", "backend_port")
1587+
1588+ @property
1589+ def media_folders(self):
1590+ '''Return a list of folders for media.'''
1591+ media = self.content.get("Media", "folders")
1592+ return self._is_valid_media_folder(media.split(';'))
1593+
1594+ @property
1595+ def feeds(self):
1596+ '''List of feeds.'''
1597+ rss_feeds = self.content.get("RSS", "feeds")
1598+ return rss_feeds.split(';')
1599+
1600+ @property
1601+ def weather_location(self):
1602+ '''User's weather location.'''
1603+ return self.content.get("Weather", "location")
1604+
1605+ @property
1606+ def display_weather_in_client(self):
1607+ '''Boolean to tell if weather should be displayed.'''
1608+ return self.content.getboolean("Weather", "display_in_menu")
1609+
1610+ @property
1611+ def download_metadata(self):
1612+ '''Boolean to tell if metadata should be downloaded.'''
1613+ return self.content.getboolean("Media", "download_metadata")
1614+
1615+ @property
1616 def download_album_art(self):
1617- """
1618- Download album art from the Internet.
1619- @return: boolean
1620- """
1621- try:
1622- result = self.content_config.getboolean("Music",
1623- "download_album_art")
1624- except (NoSectionError, NoOptionError):
1625- return False
1626- return result
1627+ '''Boolean to tell if album art should be downloaded.'''
1628+ return self.content.getboolean("Media", "download_album_art")
1629
1630+ @property
1631 def download_lyrics(self):
1632- """
1633- Download song lyrics from the Internet.
1634- @return: boolean
1635- """
1636- try:
1637- result = self.content_config.getboolean("Music", "download_lyrics")
1638- except (NoSectionError, NoOptionError):
1639- return False
1640- return result
1641-
1642- def display_hidden_files_folders(self):
1643- """If True hidden files and folders will be added to image library."""
1644- try:
1645- result = self.content_config.getboolean("Images",
1646- "display_hidden_files_folders")
1647- except (NoSectionError, NoOptionError):
1648- return False
1649- return result
1650-
1651+ '''Boolean to tell if lyrics should be downloaded.'''
1652+ return self.content.getboolean("Media", "download_lyrics")
1653+
1654+ @property
1655 def show_effects(self):
1656- """
1657- Use animations on user interface.
1658- @return: boolean
1659- """
1660- try:
1661- result = self.preferences.getboolean("General", "show_effects")
1662- except (NoSectionError, NoOptionError):
1663- return False
1664- return result
1665+ '''Boolean to tell if effects should be shown.'''
1666+ return self.content.getboolean("General", "show_effects")
1667
1668+ @property
1669 def transition_effect(self):
1670- """
1671- Which transition effect should be used when switching screens.
1672- @return: string (name of the effect)
1673- """
1674- try:
1675- result = self.preferences.get("General", "transition_effect")
1676- except (NoSectionError, NoOptionError):
1677- return False
1678- return result
1679-
1680- def get_theme_name(self):
1681- """
1682- Get the name of the current theme.
1683- @return: string (theme name)
1684- """
1685- try:
1686- result = self.preferences.get("General", "theme")
1687- except (NoSectionError, NoOptionError):
1688- result = "Default"
1689- return result
1690-
1691+ '''Internal name of the user's selected screen transition style.'''
1692+ return self.content.get("General", "transition_effect")
1693+
1694+ @property
1695+ def theme_name(self):
1696+ '''Name of the current theme.'''
1697+ return self.content.get("General", "theme")
1698+
1699+ @property
1700 def start_in_fullscreen(self):
1701 '''Boolean to determine whether to start in fullscreen mode or not.'''
1702- try:
1703- result = self.preferences.getboolean("General",
1704- "start_in_fullscreen")
1705- except (NoSectionError, NoOptionError):
1706- return True
1707- return result
1708+ return self.content.getboolean("General", "start_in_fullscreen")
1709
1710+ @property
1711 def start_auto_server(self):
1712- """
1713- Return True if backend server should auto start
1714- @return: boolean
1715- """
1716- try:
1717- result = self.preferences.getboolean("General",
1718- "start_server_auto")
1719- except (NoSectionError, NoOptionError):
1720- return False
1721- return result
1722+ '''Boolean to tell if the server should be auto started.'''
1723+ return self.content.getboolean("General", "start_server_auto")
1724
1725+ @property
1726 def history_size(self):
1727- """
1728- Get screen history size. This determines how many screen are kept in
1729- memory when scrolling.
1730- @return: Integer
1731- """
1732- try:
1733- result = self.preferences.getint("General", "history_size")
1734- except (NoSectionError, NoOptionError):
1735- return 10
1736- return result
1737+ '''Number of screens to hold in history.'''
1738+ return self.content.getint("General", "history_size")
1739
1740- def get_slideshow_step(self):
1741- """
1742- Get the slideshow step in seconds used by the photo slideshow
1743- @return: Integer
1744- """
1745- try:
1746- result = self.preferences.getint("Photographs", "slideshow_step")
1747- except (NoSectionError, NoOptionError):
1748- return 5
1749- return result
1750+ @property
1751+ def slideshow_step(self):
1752+ '''Amount of seconds before the slideshow steps to the next image.'''
1753+ return self.content.getint("Photographs", "slideshow_step")
1754
1755 # Implements MessageHandler interface
1756 def handleMessage(self, message):
1757@@ -480,73 +222,99 @@
1758 """
1759 if message.get_type() == MessageType.CONTENT_CONF_UPDATED:
1760 self.update_configuration()
1761- elif message.get_type() == MessageType.PREFERENCES_CONF_UPDATED:
1762- self.update_configuration()
1763-
1764- def get_cfg_dir(self):
1765- """
1766- Get the entertainer configuration dir.
1767- @return: strings
1768- """
1769- return self.cfg_dir
1770-
1771- def taint(self, kind, section, option):
1772- '''Taint any data that is written to config files so that it can be
1773- returned to a known state later'''
1774- if kind == 'content':
1775- value = self.content_config.get(section, option)
1776- elif kind == 'preferences':
1777- value = self.preferences.get(section, option)
1778- else:
1779- raise Exception(
1780- "Taint type must be either 'content' or 'preferences'")
1781-
1782- key = "%s %s %s" % (kind, section, option)
1783- self._tainted[key] = value
1784-
1785- def taint_in_memory(self, callback, kwargs):
1786- '''Taint any data that is stored in memory. Callback is used as the
1787- key to a tainted_in_memory dictionary whose stored value is a
1788- dictionary of arguments (kwargs) needed to restore the original
1789- value'''
1790- self._tainted_in_memory[callback] = kwargs
1791-
1792- def sanitize(self):
1793- '''Restore all tainted values to their original state and clear the
1794- tainted dictionary'''
1795- # Sanitize values in config files
1796- for key in self._tainted.keys():
1797- # tokens will contain type, section, and option
1798- tokens = key.split()
1799- if tokens[0] == 'content':
1800- # Write in the sanitize mode so taint checking will be skipped
1801- self.write_content_value(tokens[1], tokens[2],
1802- self._tainted[key], sanitize=True)
1803- elif tokens[0] == 'preferences':
1804- # Write in the sanitize mode so taint checking will be skipped
1805- self.write_preference_value(tokens[1], tokens[2],
1806- self._tainted[key], sanitize=True)
1807-
1808- self.update_configuration()
1809- self._tainted = {}
1810-
1811- # Sanitize values in memory
1812- for callback in self._tainted_in_memory.keys():
1813- kwargs = self._tainted_in_memory[callback]
1814- callback(**kwargs)
1815-
1816- self._tainted_in_memory = {}
1817
1818 def _is_valid_media_folder(self, folder_list):
1819 """Return a folder list where eventual cache folders are removed"""
1820- cache_dir = os.path.join(self.get_cfg_dir(), 'cache')
1821 valid_folder_list = []
1822
1823 for folder in folder_list:
1824- common_prefix = os.path.commonprefix([cache_dir, folder])
1825- if common_prefix != cache_dir:
1826- # folder is not a subfolder of cache_dir we accept it
1827+ common_prefix = os.path.commonprefix([self.cache_dir, folder])
1828+ if common_prefix != self.cache_dir:
1829+ # if folder is not a subfolder of cache_dir, we accept it
1830 valid_folder_list.append(folder)
1831
1832 return valid_folder_list
1833
1834+
1835+class Resources(object):
1836+ '''A Wrapper for the XDG directories. Also handles creation of a new setup
1837+ if the Entertainer directories within the XDG directories are missing. This
1838+ class is meant solely to support Configuration and should NOT be publicly
1839+ used because of testing conflicts that would occur.'''
1840+
1841+ def __init__(self, resource='entertainer', config_testing_dir=None):
1842+ if config_testing_dir is None:
1843+ self.cache_dir = os.path.join(BaseDirectory.xdg_cache_home,
1844+ resource)
1845+ self.config_dir = os.path.join(BaseDirectory.xdg_config_home,
1846+ resource)
1847+ self.data_dir = os.path.join(BaseDirectory.xdg_data_home, resource)
1848+ else:
1849+ # Running in the test suite so don't create the XDG locations.
1850+ self.cache_dir = os.path.join(config_testing_dir, 'cache')
1851+ self.config_dir = os.path.join(config_testing_dir, 'config')
1852+ self.data_dir = os.path.join(config_testing_dir, 'data')
1853+
1854+ # Ensure that the directories exist.
1855+ if not os.path.exists(self.cache_dir):
1856+ self.create_cache_hierarchy()
1857+ if not os.path.exists(self.config_dir):
1858+ self.create_configuration()
1859+ if not os.path.exists(self.data_dir):
1860+ self.create_initial_data()
1861+
1862+ def create_cache_hierarchy(self):
1863+ '''Create the cache hierarchy that is assumed to exist.'''
1864+ os.makedirs(os.path.join(self.cache_dir, 'album_art'))
1865+ os.makedirs(os.path.join(self.cache_dir, 'movie_art'))
1866+ os.makedirs(os.path.join(self.cache_dir, 'thumbnails', 'image'))
1867+ os.makedirs(os.path.join(self.cache_dir, 'thumbnails', 'video'))
1868+
1869+ def create_configuration(self):
1870+ '''Create the user's configuration area and populate with the default
1871+ content configuration.'''
1872+
1873+ # Copy configuration data from a dev branch if we can
1874+ dev_config = os.path.abspath(os.path.dirname(__file__) + '/../cfg')
1875+ if os.path.exists(dev_config):
1876+ shutil.copytree(dev_config, self.config_dir)
1877+ return
1878+
1879+ # Must be a proper installation so install from the system location.
1880+ installed_config = os.path.join(self.installed_data_dir, 'cfg')
1881+ shutil.copytree(installed_config, self.config_dir)
1882+
1883+ def create_initial_data(self):
1884+ '''Create the initial data directory and populate with the default data
1885+ used by Entertainer.'''
1886+ os.mkdir(self.data_dir)
1887+
1888+ # Copy configuration data from a dev branch if we can
1889+ dev_config = os.path.abspath(os.path.dirname(__file__) + '/../themes')
1890+ if os.path.exists(dev_config):
1891+ shutil.copytree(dev_config, os.path.join(self.data_dir, 'themes'))
1892+ return
1893+
1894+ # Must be a proper installation so install from the system location.
1895+ themes_dir = os.path.join(self.installed_data_dir, 'themes')
1896+ shutil.copytree(themes_dir, os.path.join(self.data_dir, 'themes'))
1897+
1898+ @property
1899+ def installed_data_dir(self):
1900+ '''Since different distros decide on the install path differently, scan
1901+ through the system data directories to find where Entertainer was
1902+ installed.'''
1903+
1904+ # Get rid of the user's home data directory because we don't want it.
1905+ system_data_dirs = [data_dir for data_dir in BaseDirectory.xdg_data_dirs
1906+ if not data_dir.startswith(BaseDirectory.xdg_data_home)]
1907+
1908+ installed_data_dir = None
1909+ for directory in system_data_dirs:
1910+ possible_data_dir = os.path.join(directory, 'entertainer')
1911+ if os.path.exists(possible_data_dir):
1912+ installed_data_dir = possible_data_dir
1913+ break
1914+
1915+ return installed_data_dir
1916+
1917
1918=== renamed directory 'entertainerlib/backend/core/db' => 'entertainerlib/db'
1919=== modified file 'entertainerlib/db/connection.py'
1920--- entertainerlib/backend/core/db/connection.py 2009-05-11 23:37:27 +0000
1921+++ entertainerlib/db/connection.py 2009-06-17 02:53:31 +0000
1922@@ -27,7 +27,7 @@
1923 'PhotoImage': '''CREATE TABLE `photoimage` (
1924 id INTEGER PRIMARY KEY AUTOINCREMENT,
1925 filename TEXT,
1926- thumb VARCHAR(32),
1927+ thumbnail VARCHAR(32),
1928 title TEXT,
1929 description TEXT,
1930 creation_date DATETIME,
1931
1932=== modified file 'entertainerlib/db/models.py'
1933--- entertainerlib/backend/core/db/models.py 2009-04-28 23:54:49 +0000
1934+++ entertainerlib/db/models.py 2009-06-17 02:53:31 +0000
1935@@ -6,6 +6,7 @@
1936 from storm.properties import Bool, DateTime, Int, Unicode
1937 from storm.references import Reference, ReferenceSet
1938
1939+
1940 class BaseModel(Storm):
1941 '''Abstract class from which all Entertainer models inherit.'''
1942
1943@@ -96,7 +97,7 @@
1944 __storm_table__ = 'photoimage'
1945 id = Int(primary=True)
1946 filename = Unicode()
1947- thumb = Unicode()
1948+ thumbnail = Unicode()
1949 title = Unicode()
1950 description = Unicode()
1951 creation_date = DateTime()
1952@@ -108,7 +109,7 @@
1953 ret = {
1954 'id': self.id,
1955 'filename': self.filename,
1956- 'thumb': self.thumb,
1957+ 'thumbnail': self.thumbnail,
1958 'title': self.title,
1959 'description': self.description,
1960 'creation_date': self.creation_date,
1961
1962=== added file 'entertainerlib/dialog.py'
1963--- entertainerlib/dialog.py 1970-01-01 00:00:00 +0000
1964+++ entertainerlib/dialog.py 2009-08-27 03:18:41 +0000
1965@@ -0,0 +1,1110 @@
1966+# Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
1967+'''Dialogs for Entertainer.'''
1968+# pylint: disable-msg=C0302
1969+
1970+import os
1971+import shutil
1972+import socket
1973+import sys
1974+import tarfile
1975+
1976+import gtk
1977+import gtk.glade
1978+
1979+from entertainerlib.backend.core.message import Message
1980+from entertainerlib.backend.core.message_bus_proxy import MessageBusProxy
1981+from entertainerlib.backend.core.message_type_priority import MessageType
1982+from entertainerlib.configuration import Configuration
1983+from entertainerlib.logger import Logger
1984+from entertainerlib.gui.theme import Theme
1985+from entertainerlib.backend.components.feeds.feed_utils import (OPMLParser,
1986+ FeedConfigTools)
1987+from entertainerlib.weather import Weather
1988+
1989+
1990+class ManagerDialog:
1991+ """
1992+ This is a content management tool for Entertainer media center application.
1993+ """
1994+
1995+ # Temporary storage for entered URL
1996+ url = ""
1997+ GLADE_DIR = os.path.join(os.path.dirname(__file__), "glade")
1998+
1999+ def __init__(self, stand_alone):
2000+ """
2001+ Initialize content management dialog
2002+ @param stand_alone: Boolean, Is this dialog running as a stand alone
2003+ process
2004+ """
2005+ self.stand_alone = stand_alone
2006+ self.config = Configuration()
2007+ self.themes = []
2008+ self.weather = Weather()
2009+
2010+ # Load glade UI
2011+ self.gladefile = os.path.join(self.GLADE_DIR, 'manager.glade')
2012+ self.widgets = gtk.glade.XML(self.gladefile)
2013+
2014+ # Get content management dialog and bind signal callbacks
2015+ self.dialog = self.widgets.get_widget("ManagerDialog")
2016+ if (self.dialog):
2017+ callback_dic = {
2018+ # Dialog-wide callbacks
2019+ "on_button_open_list_clicked" :
2020+ self.on_button_open_list_clicked,
2021+ "on_close_button_clicked" : self.on_close_button_clicked,
2022+ "on_ManagerDialog_destroy" : self.on_dialog_closed,
2023+
2024+ # Media tab
2025+ "on_button_remove_media_clicked" :
2026+ self.on_button_remove_media_clicked,
2027+ "on_button_add_media_clicked" :
2028+ self.on_button_add_media_clicked,
2029+ "on_button_edit_media_clicked" :
2030+ self.on_button_edit_media_clicked,
2031+ "on_checkbutton_video_metadata_toggled" :
2032+ self.on_checkbutton_video_metadata_toggled,
2033+ "on_lyrics_checkbox_toggled" : self.on_lyrics_checkbox_toggled,
2034+ "on_art_checkbox_toggled" : self.on_art_checkbox_toggled,
2035+ "on_button_media_rebuild_clicked" :
2036+ self.on_button_media_rebuild_clicked,
2037+
2038+ # Feed tab
2039+ "on_button_add_feed_clicked" :
2040+ self.on_button_add_feed_clicked,
2041+ "on_button_remove_feed_clicked" :
2042+ self.on_button_remove_feed_clicked,
2043+ "on_button_edit_feed_clicked" :
2044+ self.on_button_edit_feed_clicked,
2045+ "on_fetch_interval_spinbutton_value_changed" :
2046+ self.on_fetch_interval_spinbutton_value_changed,
2047+ "on_url_dialog_delete_event" : self.on_url_dialog_delete_event,
2048+ "on_url_dialog_ok_button_clicked" :
2049+ self.on_url_dialog_ok_button_clicked,
2050+ "on_url_dialog_cancel_button_clicked" :
2051+ self.on_url_dialog_cancel_button_clicked,
2052+ "on_button_feed_rebuild_clicked" :
2053+ self.on_button_feed_rebuild_clicked,
2054+
2055+ # Weather tab
2056+ "on_button_add_weather_clicked" :
2057+ self.on_button_add_weather_clicked,
2058+ "on_button_remove_weather_clicked" :
2059+ self.on_button_remove_weather_clicked,
2060+ "on_weather_display_checkbox_toggled" :
2061+ self.on_weather_display_checkbox_toggled,
2062+ "on_location_find_button_clicked" :
2063+ self.on_location_find_button_clicked,
2064+ "on_location_cancel_button_clicked" :
2065+ self.on_location_cancel_button_clicked,
2066+ "on_location_add_button_clicked" :
2067+ self.on_location_add_button_clicked,
2068+ "on_location_entry_activate" : self.on_location_entry_activate,
2069+
2070+ # User Interface tab
2071+ "on_theme_list_cursor_changed" :
2072+ self.on_theme_list_cursor_changed,
2073+ "on_theme_add_button_clicked" :
2074+ self.on_theme_add_button_clicked,
2075+ "on_theme_remove_button_clicked" :
2076+ self.on_theme_remove_button_clicked,
2077+ "on_checkbutton_effects_toggled" :
2078+ self.on_checkbutton_effects_toggled,
2079+ "on_combobox_effects_changed" :
2080+ self.on_combobox_effects_changed,
2081+
2082+ # General tab
2083+ "on_checkbutton_fullscreen_toggled" :
2084+ self.on_checkbutton_fullscreen_toggled,
2085+ "on_checkbutton_autostart_toggled" :
2086+ self.on_checkbutton_autostart_toggled,
2087+ "on_checkbutton_systray_icon_toggled" :
2088+ self.on_checkbutton_systray_icon_toggled,
2089+ "on_spinbutton_slideshow_step_value_changed":
2090+ self.on_spinbutton_slideshow_step_value_changed
2091+ }
2092+
2093+ self.widgets.signal_autoconnect(callback_dic)
2094+
2095+ # Initialize dialog widgets with correct values and show dialog
2096+ self.init_dialog_values_from_configure_file()
2097+ self.dialog.resize(500, 300)
2098+ self.dialog.show()
2099+
2100+ # Initialize location list in search dialog
2101+ result_list = self.widgets.get_widget("location_results_treeview")
2102+ store = gtk.ListStore(str)
2103+ result_list.set_model(store)
2104+ cell_renderer = gtk.CellRendererText()
2105+ column = gtk.TreeViewColumn(_("Location"), cell_renderer, text=0)
2106+ result_list.append_column(column)
2107+
2108+ def on_dialog_closed(self, widget):
2109+ """Callback function for dialog's close button"""
2110+ try:
2111+ proxy = MessageBusProxy(client_name = "Manager GUI")
2112+ proxy.connectToMessageBus()
2113+ proxy.sendMessage(Message(MessageType.CONTENT_CONF_UPDATED))
2114+ proxy.disconnectFromMessageBus()
2115+ except socket.error:
2116+ error = gtk.MessageDialog(
2117+ None, gtk.DIALOG_MODAL,
2118+ gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _(
2119+ "Entertainer backend is not running. "
2120+ "Cache cannot be rebuilt."
2121+ ))
2122+ error.run()
2123+ error.destroy()
2124+
2125+ if(self.stand_alone):
2126+ self.dialog.hide()
2127+ self.dialog.destroy()
2128+ gtk.main_quit()
2129+ else:
2130+ self.dialog.hide()
2131+ self.dialog.destroy()
2132+
2133+ def on_close_button_clicked(self, widget):
2134+ """Callback function for dialog's close button"""
2135+ if(self.stand_alone):
2136+ self.dialog.hide()
2137+ self.dialog.destroy()
2138+ gtk.main_quit()
2139+ else:
2140+ self.dialog.hide()
2141+ self.dialog.destroy()
2142+
2143+ def on_button_add_media_clicked(self, widget):
2144+ """Opens add URL dialog. """
2145+ widget = self.widgets.get_widget("treeview_media")
2146+ model = widget.get_model()
2147+ # Open "Select folder" dialog
2148+ dialog = gtk.FileChooserDialog(_("Select folder"), None,
2149+ gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
2150+ (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,
2151+ gtk.STOCK_OPEN,gtk.RESPONSE_OK),
2152+ None)
2153+ status = dialog.run()
2154+ # If folder was selected we add it to model and update config file
2155+ if(status == gtk.RESPONSE_OK):
2156+ self.add_to_model_and_config(dialog.get_current_folder(), model,
2157+ self.media_folders, "Media")
2158+ dialog.destroy()
2159+
2160+ def on_button_remove_media_clicked(self, widget):
2161+ """Remove currently selected folder from media folders"""
2162+ widget = self.widgets.get_widget("treeview_media")
2163+ model = widget.get_model()
2164+ selection = widget.get_selection().get_selected()
2165+ if selection[1] == None:
2166+ return
2167+ rm_folder = model.get_value(selection[1], 0)
2168+ self.media_folders.remove(rm_folder)
2169+ str_folders = ";".join(self.media_folders)
2170+ self.config.write_content_value("Media", "folders", str_folders)
2171+ model.remove(selection[1])
2172+
2173+ def on_button_edit_media_clicked(self, widget):
2174+ """Edit currently selected folder"""
2175+ widget = self.widgets.get_widget("treeview_media")
2176+ url_dialog = self.widgets.get_widget("url_dialog")
2177+ url_entry = self.widgets.get_widget("url_entry")
2178+ model = widget.get_model()
2179+ selection = widget.get_selection().get_selected()
2180+ if selection[1] == None:
2181+ return
2182+ folder = model.get_value(selection[1], 0)
2183+ url_entry.set_text(folder)
2184+ url_dialog.set_title(_("Edit URL"))
2185+ status = url_dialog.run()
2186+ if status == gtk.RESPONSE_OK and os.path.exists(self.url):
2187+ # Update list model
2188+ model.set_value(selection[1], 0, self.url)
2189+ # Update configure file
2190+ pos = self.media_folders.index(folder)
2191+ self.media_folders.remove(folder)
2192+ self.media_folders.insert(pos, self.url)
2193+ str_folders = ";".join(self.media_folders)
2194+ self.config.write_content_value("Media", "folders",
2195+ str_folders)
2196+
2197+ def on_checkbutton_autostart_toggled(self, widget):
2198+ '''Server autostart checkbox toggled.'''
2199+ self.config.write_content_value("General", "start_server_auto",
2200+ widget.get_active())
2201+
2202+ def on_checkbutton_fullscreen_toggled(self, widget):
2203+ '''Start in fullscreen checkbox toggled.'''
2204+ self.config.write_content_value("General", "start_in_fullscreen",
2205+ widget.get_active())
2206+
2207+ def on_checkbutton_systray_icon_toggled(self, widget):
2208+ """System Tray Icon checkbox toggled"""
2209+ self.config.write_content_value("General", "display_icon",
2210+ widget.get_active())
2211+
2212+ def on_checkbutton_video_metadata_toggled(self, widget):
2213+ """
2214+ Download video file metadata from internet
2215+ @param widget: GTK-Widget
2216+ """
2217+ self.config.write_content_value("Media", "download_metadata",
2218+ widget.get_active())
2219+
2220+ def on_button_add_feed_clicked(self, widget):
2221+ """Opens add feed dialog. """
2222+ widget = self.widgets.get_widget("treeview_feeds")
2223+ url_dialog = self.widgets.get_widget("url_dialog")
2224+ model = widget.get_model()
2225+ # Open dialog
2226+ url_dialog.set_title(_("Add RSS-feed"))
2227+ status = url_dialog.run()
2228+ # If folder was selected we add it to model and update config file
2229+ if(status == gtk.RESPONSE_OK):
2230+ model.append([self.url])
2231+ self.feeds.append(self.url)
2232+ str_folders = ";".join(self.feeds)
2233+ self.config.write_content_value("RSS", "feeds", str_folders)
2234+
2235+ def on_button_remove_feed_clicked(self, widget):
2236+ """Remove currently selected reed from RSS-feeds"""
2237+ widget = self.widgets.get_widget("treeview_feeds")
2238+ model = widget.get_model()
2239+ selection = widget.get_selection().get_selected()
2240+ if selection[1] == None:
2241+ return
2242+ rm_folder = model.get_value(selection[1], 0)
2243+ self.feeds.remove(rm_folder)
2244+ str_folders = ";".join(self.feeds)
2245+ self.config.write_content_value("RSS", "feeds", str_folders)
2246+ model.remove(selection[1])
2247+
2248+ def on_button_edit_feed_clicked(self, widget):
2249+ """Edit currently selected feed"""
2250+ widget = self.widgets.get_widget("treeview_feeds")
2251+ url_dialog = self.widgets.get_widget("url_dialog")
2252+ url_entry = self.widgets.get_widget("url_entry")
2253+ model = widget.get_model()
2254+ selection = widget.get_selection().get_selected()
2255+ if selection[1] == None:
2256+ return
2257+ feed = model.get_value(selection[1], 0)
2258+ url_entry.set_text(feed)
2259+ url_dialog.set_title(_("Edit feed"))
2260+ status = url_dialog.run()
2261+ if status == gtk.RESPONSE_OK:
2262+ # Update list model
2263+ model.set_value(selection[1], 0, self.url)
2264+ # Update configure file
2265+ pos = self.feeds.index(feed)
2266+ self.feeds.remove(feed)
2267+ self.feeds.insert(pos, self.url)
2268+ str_feeds = ";".join(self.feeds)
2269+ self.config.write_content_value("RSS", "feeds", str_feeds)
2270+
2271+ def on_button_open_list_clicked(self, widget):
2272+ """Opens the open feed source dialog"""
2273+ open_dialog = OpenFeedSourceDialog(
2274+ self.widgets.get_widget("treeview_feeds"), self.feeds)
2275+ open_dialog.dialog.connect("destroy", open.on_closeButton_clicked)
2276+ open_dialog.dialog.hide()
2277+ open_dialog.dialog.destroy()
2278+
2279+ def on_fetch_interval_spinbutton_value_changed(self, widget):
2280+ self.config.write_content_value("RSS", "fetch_interval",
2281+ widget.get_value_as_int())
2282+
2283+ def on_spinbutton_slideshow_step_value_changed(self, widget):
2284+ """Activation of slideshow effects"""
2285+ self.config.write_content_value("Photographs", "slideshow_step",
2286+ int(widget.get_value()))
2287+
2288+ def on_lyrics_checkbox_toggled(self, widget):
2289+ self.config.write_content_value("Media", "download_lyrics",
2290+ widget.get_active())
2291+
2292+ def on_art_checkbox_toggled(self, widget):
2293+ self.config.write_content_value("Media", "download_album_art",
2294+ widget.get_active())
2295+
2296+ def on_url_dialog_ok_button_clicked(self, widget):
2297+ """URL dialog OK button pressed. Sets self.url"""
2298+ url_dialog = self.widgets.get_widget("url_dialog")
2299+ url_entry = self.widgets.get_widget("url_entry")
2300+ url_dialog.hide()
2301+ self.url = url_entry.get_text()
2302+ url_entry.set_text("")
2303+ url_dialog.response(gtk.RESPONSE_OK)
2304+
2305+ def on_url_dialog_cancel_button_clicked(self, widget):
2306+ """URL dialog cancelled. Hides dialog"""
2307+ url_dialog = self.widgets.get_widget("url_dialog")
2308+ url_entry = self.widgets.get_widget("url_entry")
2309+ url_dialog.hide()
2310+ url_entry.set_text("")
2311+ url_dialog.response(gtk.RESPONSE_CANCEL)
2312+
2313+ def on_url_dialog_delete_event(self, widget, data):
2314+ """Dialog's X clicked. Hides dialog"""
2315+ url_dialog = self.widgets.get_widget("url_dialog")
2316+ url_entry = self.widgets.get_widget("url_entry")
2317+ url_dialog.hide()
2318+ url_entry.set_text("")
2319+ url_dialog.response(gtk.RESPONSE_CANCEL)
2320+ return True
2321+
2322+ def on_button_add_weather_clicked(self, widget):
2323+ """
2324+ Open location search dialog
2325+ @param widget: GTK-Widget
2326+ """
2327+ location_dialog = self.widgets.get_widget("weather_search_dialog")
2328+ location_dialog.set_title(_("Add location"))
2329+
2330+ # Clear results
2331+ result_list = self.widgets.get_widget("location_results_treeview")
2332+ model = result_list.get_model()
2333+ model.clear()
2334+
2335+ status = location_dialog.run()
2336+ if(status == gtk.RESPONSE_OK):
2337+ print "Added"
2338+
2339+ def on_button_remove_weather_clicked(self, widget):
2340+ """
2341+ Remove currently selected weather location from the location list
2342+ @param widget: GTK-Widget
2343+ """
2344+ widget = self.widgets.get_widget("treeview_locations")
2345+ model = widget.get_model()
2346+ self.weather_locations = []
2347+ str_folders = ""
2348+ self.config.write_content_value("Weather", "location", str_folders)
2349+ model.clear()
2350+
2351+ def on_weather_display_checkbox_toggled(self, widget):
2352+ """
2353+ Checkbox that defines should we use weather conditions
2354+ @param widget: GTK-Widget
2355+ """
2356+ self.config.write_content_value("Weather", "display_in_menu",
2357+ widget.get_active())
2358+ if widget.get_active():
2359+ self.widgets.get_widget("button_add_weather").set_sensitive(True)
2360+ self.widgets.get_widget(
2361+ "button_remove_weather").set_sensitive(True)
2362+ self.widgets.get_widget("treeview_locations").set_sensitive(True)
2363+ else:
2364+ self.widgets.get_widget("button_add_weather").set_sensitive(False)
2365+ self.widgets.get_widget(
2366+ "button_remove_weather").set_sensitive(False)
2367+ self.widgets.get_widget("treeview_locations").set_sensitive(False)
2368+
2369+ def on_location_find_button_clicked(self, widget):
2370+ """
2371+ Find location by search string
2372+ @param widget: GTK-Widget
2373+ """
2374+ add_button = self.widgets.get_widget("location_add_button")
2375+ search_term = self.widgets.get_widget("location_entry").get_text()
2376+ result_list = self.widgets.get_widget("location_results_treeview")
2377+ model = result_list.get_model()
2378+ model.clear()
2379+ if search_term != "":
2380+ self.weather.location = search_term
2381+ self.weather.refresh()
2382+ results = self.weather.forecasts
2383+ if len(results) > 0:
2384+ add_button.set_sensitive(True)
2385+ model.append([search_term])
2386+ result_list.set_cursor(0)
2387+ else:
2388+ model.clear()
2389+ model.append([_("Location Not Found!")])
2390+ add_button.set_sensitive(False)
2391+
2392+ def on_location_cancel_button_clicked(self, widget):
2393+ """
2394+ Close location search dialog without taking any actions.0
2395+ @param widget: GTK-Widget
2396+ """
2397+ location_dialog = self.widgets.get_widget("weather_search_dialog")
2398+ location_entry = self.widgets.get_widget("location_entry")
2399+ location_dialog.hide()
2400+ location_entry.set_text("")
2401+ location_dialog.response(gtk.RESPONSE_CANCEL)
2402+
2403+ def on_location_add_button_clicked(self, widget):
2404+ """
2405+ Add selected location to location list and close search dialog
2406+ @param widget: GTK-Widget
2407+ """
2408+ self.weather_locations = []
2409+ result_list = self.widgets.get_widget("location_results_treeview")
2410+ model = result_list.get_model()
2411+ selection = result_list.get_selection().get_selected()
2412+ if selection[1] == None:
2413+ return
2414+ location_string = model.get_value(selection[1], 0)
2415+
2416+ location_list = self.widgets.get_widget("treeview_locations")
2417+ loc_model = location_list.get_model()
2418+ loc_model.clear()
2419+ loc_model.append([location_string])
2420+
2421+ self.weather_locations.append(location_string)
2422+ str_locations = ";".join(self.weather_locations)
2423+ self.config.write_content_value("Weather", "location", str_locations)
2424+
2425+ location_dialog = self.widgets.get_widget("weather_search_dialog")
2426+ location_entry = self.widgets.get_widget("location_entry")
2427+ location_dialog.hide()
2428+ location_entry.set_text("")
2429+ location_dialog.response(gtk.RESPONSE_CANCEL)
2430+
2431+ def on_location_entry_activate(self, widget):
2432+ """
2433+ User hit enter on location entry to start search
2434+ @param widget: GTK-Widget
2435+ """
2436+ self.on_location_find_button_clicked(widget)
2437+
2438+ def on_button_media_rebuild_clicked(self, widget):
2439+ '''Rebuild media cache requested.'''
2440+ try:
2441+ proxy = MessageBusProxy(client_name = "Manager GUI")
2442+ proxy.connectToMessageBus()
2443+ proxy.sendMessage(Message(MessageType.REBUILD_IMAGE_CACHE))
2444+ proxy.sendMessage(Message(MessageType.REBUILD_MUSIC_CACHE))
2445+ proxy.sendMessage(Message(MessageType.REBUILD_VIDEO_CACHE))
2446+ proxy.disconnectFromMessageBus()
2447+ except socket.error:
2448+ error = gtk.MessageDialog(
2449+ None, gtk.DIALOG_MODAL,
2450+ gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _(
2451+ "Entertainer backend is not running. "
2452+ "Cache cannot be rebuilt."
2453+ ))
2454+ error.run()
2455+ error.destroy()
2456+
2457+ def on_button_feed_rebuild_clicked(self, widget):
2458+ """
2459+ Rebuild feed cache requested
2460+ @param widget: GTK-Widget
2461+ """
2462+ #We need the user to confirm the rebuild feed cache request
2463+ dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING,
2464+ gtk.BUTTONS_OK_CANCEL,
2465+ _("This will completely remove any feed entries in the cache!"))
2466+ status = dialog.run()
2467+ #If user has ok'd the request send the message to the message bus
2468+ if(status == gtk.RESPONSE_OK):
2469+ try:
2470+ proxy = MessageBusProxy(client_name = "Manager GUI")
2471+ proxy.connectToMessageBus()
2472+ proxy.sendMessage(Message(MessageType.REBUILD_FEED_CACHE))
2473+ proxy.disconnectFromMessageBus()
2474+ except socket.error:
2475+ error = gtk.MessageDialog(
2476+ None, gtk.DIALOG_MODAL,
2477+ gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _(
2478+ "Entertainer backend is not running. "
2479+ "Cache cannot be rebuilt."
2480+ ))
2481+ error.run()
2482+ error.destroy()
2483+
2484+ dialog.destroy()
2485+
2486+ def on_theme_add_button_clicked(self, widget):
2487+ """Add theme button clicked"""
2488+ themelist = self.widgets.get_widget("theme_list")
2489+ model = themelist.get_model()
2490+ # Open "Select folder" dialog
2491+ dialog = gtk.FileChooserDialog(_("Select theme package file"),
2492+ None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL,
2493+ gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK), None)
2494+ file_filter = gtk.FileFilter()
2495+ file_filter.set_name(_("Theme package (tar.gz)"))
2496+ file_filter.add_pattern("*.tar.gz")
2497+ dialog.add_filter(file_filter)
2498+ status = dialog.run()
2499+
2500+ # If theme was selected with file chooser
2501+ if(status == gtk.RESPONSE_OK):
2502+ package = dialog.get_filename()
2503+ tar = tarfile.open(package, 'r:gz') # Open tar.gz package
2504+
2505+ # Make sure that package contains configuration file (is theme)
2506+ content = tar.getnames()
2507+ theme_name = None
2508+ is_theme = False
2509+ for element in content:
2510+ if element[-10:] == "theme.conf":
2511+ theme_name = element[:-11]
2512+ is_theme = True
2513+
2514+ # Install them
2515+ if is_theme:
2516+ tar.extractall(os.path.join(self.config.data_dir, 'themes'))
2517+ model.insert(len(model), [theme_name])
2518+ else:
2519+ error = gtk.MessageDialog(None, gtk.DIALOG_MODAL,
2520+ gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("Invalid theme file!"))
2521+ error.run()
2522+ error.destroy()
2523+
2524+ dialog.destroy()
2525+
2526+ def on_theme_list_cursor_changed(self, widget):
2527+ """Executed when theme is changed in theme list. Update preview."""
2528+ # Get currently selected theme
2529+ themelist = self.widgets.get_widget("theme_list")
2530+ model = themelist.get_model()
2531+ selection = themelist.get_selection().get_selected()
2532+ name = model.get_value(selection[1], 0)
2533+ themedir = os.path.join(self.config.data_dir, 'themes', name)
2534+ theme = Theme(theme_path=themedir)
2535+
2536+ # Update preview
2537+ image = self.widgets.get_widget("theme_image")
2538+ image.set_from_file(os.path.join(themedir, "thumbnail.png"))
2539+ name = self.widgets.get_widget("name_label")
2540+ name.set_text(theme.getName())
2541+ author = self.widgets.get_widget("author_label")
2542+ author.set_text(theme.getAuthor())
2543+ license_label = self.widgets.get_widget("license_label")
2544+ license_label.set_text(theme.getLicence())
2545+ copyright_label = self.widgets.get_widget("copyright_label")
2546+ copyright_label.set_text(theme.getCopyright())
2547+ comment = self.widgets.get_widget("comment_label")
2548+ comment.set_text(theme.getComment())
2549+
2550+ self.config.write_content_value("General", "theme", name.get_text())
2551+
2552+ def on_theme_remove_button_clicked(self, widget):
2553+ """Remove theme button clicked"""
2554+ # Get currently selected theme
2555+ themelist = self.widgets.get_widget("theme_list")
2556+ model = themelist.get_model()
2557+ selection = themelist.get_selection().get_selected()
2558+ name = model.get_value(selection[1], 0)
2559+
2560+ confirm = gtk.MessageDialog(None,
2561+ gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
2562+ _("Are you sure you want to delete\ntheme: %(name)s") % \
2563+ {'name': name})
2564+ status = confirm.run()
2565+ confirm.destroy()
2566+ if(status == gtk.RESPONSE_YES):
2567+ themedir = os.path.join(self.config.data_dir, 'themes', name)
2568+ shutil.rmtree(themedir)
2569+ model.remove(selection[1])
2570+ themelist.set_cursor(0)
2571+ self.themes.remove(name)
2572+
2573+ def on_checkbutton_effects_toggled(self, widget):
2574+ """Effect checkbox toggled"""
2575+ combobox = self.widgets.get_widget("combobox_effects")
2576+ combobox.set_sensitive(widget.get_active())
2577+ self.config.write_content_value("General", "show_effects",
2578+ widget.get_active())
2579+
2580+ def on_combobox_effects_changed(self, widget):
2581+ """User changed effect for screen transitions"""
2582+ text = widget.get_active_text()
2583+ if text == _("No effect"):
2584+ english_text = "No effect"
2585+ if text == _("Crossfade"):
2586+ english_text = "Crossfade"
2587+ if text == _("Zoom and fade"):
2588+ english_text = "Zoom and fade"
2589+ if text == _("Slide"):
2590+ english_text = "Slide"
2591+ self.config.write_content_value("General", "transition_effect",
2592+ english_text)
2593+
2594+ def init_dialog_values_from_configure_file(self):
2595+ """Read configuration and set dialog widget values with read values.
2596+ """
2597+ # == Videos ==
2598+ medialist_widget = self.widgets.get_widget("treeview_media")
2599+ mediastore = gtk.ListStore(str)
2600+
2601+ cell_renderer = gtk.CellRendererText()
2602+ column = gtk.TreeViewColumn(_("Folders"), cell_renderer, text=0)
2603+ medialist_widget.append_column(column)
2604+
2605+ self.media_folders = self.config.media_folders
2606+
2607+ # Fill model with folders read from config file
2608+ self.init_model(mediastore, self.media_folders)
2609+
2610+ medialist_widget.set_model(mediastore)
2611+
2612+ # Checkboxes
2613+ metadata_checkbox = self.widgets.get_widget("video_metadata_checkbox")
2614+ metadata_checkbox.set_active(self.config.download_metadata)
2615+
2616+ art_checkbox = self.widgets.get_widget("art_checkbox")
2617+ art_checkbox.set_active(self.config.download_album_art)
2618+
2619+ lyrics_checkbox = self.widgets.get_widget("lyrics_checkbox")
2620+ lyrics_checkbox.set_active(self.config.download_lyrics)
2621+
2622+ # == RSS-feeds ==
2623+ feedlist_widget = self.widgets.get_widget("treeview_feeds")
2624+ feed_model = gtk.ListStore(str)
2625+
2626+ rss_cell = gtk.CellRendererText()
2627+ rss_column = gtk.TreeViewColumn(_("Feeds"), rss_cell, text=0)
2628+ feedlist_widget.append_column(rss_column)
2629+
2630+ self.feeds = self.config.feeds
2631+
2632+ # Fill model with folders read from config file
2633+ for i in range(len(self.feeds)):
2634+ feed_model.insert(i, [self.feeds[i]])
2635+
2636+ feedlist_widget.set_model(feed_model)
2637+
2638+ # Interval spinner
2639+ interval_spinner = self.widgets.get_widget("fetch_interval_spinbutton")
2640+ interval_val = self.config.feed_fetch_interval
2641+ if interval_val < 15:
2642+ interval_val = 15
2643+ elif interval_val > 900:
2644+ interval_val = 900
2645+ interval_spinner.set_value(interval_val)
2646+
2647+ # == Weather ==
2648+ locationlist_widget = self.widgets.get_widget("treeview_locations")
2649+ location_model = gtk.ListStore(str)
2650+
2651+ loc_cell = gtk.CellRendererText()
2652+ location_column = gtk.TreeViewColumn(_("Location"), loc_cell, text=0)
2653+ locationlist_widget.append_column(location_column)
2654+
2655+ self.weather_location = self.config.weather_location
2656+
2657+ # Fill model with location read from config file
2658+ location_model.insert(0, [self.weather_location])
2659+ locationlist_widget.set_model(location_model)
2660+
2661+ weather_display_checkbox = self.widgets.get_widget(
2662+ "weather_display_checkbox")
2663+ display_val = self.config.display_weather_in_client
2664+ weather_display_checkbox.set_active(display_val)
2665+ if not display_val:
2666+ self.widgets.get_widget("button_add_weather").set_sensitive(False)
2667+ self.widgets.get_widget("button_remove_weather").set_sensitive(
2668+ False)
2669+ self.widgets.get_widget("treeview_locations").set_sensitive(False)
2670+
2671+ # == User Interface ==
2672+ self.load_themes()
2673+ current_theme = self.config.theme_name
2674+
2675+ themelist_widget = self.widgets.get_widget("theme_list")
2676+ model = gtk.ListStore(str)
2677+
2678+ cell_renderer = gtk.CellRendererText()
2679+ column = gtk.TreeViewColumn("Themes", cell_renderer, text=0)
2680+ themelist_widget.append_column(column)
2681+
2682+ # Fill model with installed themes
2683+ for i in range(len(self.themes)):
2684+ model.insert(i, [self.themes[i]])
2685+
2686+ themelist_widget.set_model(model)
2687+
2688+ # Set current theme selected in theme list
2689+ index = model.get_iter_first()
2690+ unselected = True
2691+ index_counter = 0
2692+ while(unselected):
2693+ name = model.get_value(index, 0)
2694+ if name == current_theme:
2695+ unselected = False
2696+ themelist_widget.set_cursor(index_counter)
2697+ index = model.iter_next(index)
2698+ index_counter += 1
2699+
2700+ effect_checkbox = self.widgets.get_widget("checkbutton_effects")
2701+ effect_combobox = self.widgets.get_widget("combobox_effects")
2702+ if self.config.show_effects:
2703+ effect_checkbox.set_active(True)
2704+ effect_combobox.set_sensitive(True)
2705+ else:
2706+ effect_checkbox.set_active(False)
2707+ effect_combobox.set_sensitive(False)
2708+
2709+ # Set Effect Combobox value (Text values are set in glade file)
2710+ effect = self.config.transition_effect
2711+ if effect == "No effect":
2712+ effect_combobox.set_active(0)
2713+ if effect == "Crossfade":
2714+ effect_combobox.set_active(1)
2715+ if effect == "Zoom and fade":
2716+ effect_combobox.set_active(2)
2717+ if effect == "Slide":
2718+ effect_combobox.set_active(3)
2719+
2720+ # == General ==
2721+ checkbutton_fullscreen = self.widgets.get_widget(
2722+ "checkbutton_fullscreen")
2723+ if self.config.start_in_fullscreen:
2724+ checkbutton_fullscreen.set_active(True)
2725+ else:
2726+ checkbutton_fullscreen.set_active(False)
2727+
2728+ checkbutton_autostart = self.widgets.get_widget("checkbutton_autostart")
2729+ if self.config.start_auto_server:
2730+ checkbutton_autostart.set_active(True)
2731+ else:
2732+ checkbutton_autostart.set_active(False)
2733+
2734+ checkbutton_systray_icon = self.widgets.get_widget(
2735+ "checkbutton_systray_icon")
2736+ if self.config.tray_icon_enabled:
2737+ checkbutton_systray_icon.set_active(True)
2738+ else:
2739+ checkbutton_systray_icon.set_active(False)
2740+
2741+ spinbutton_slideshow_step = self.widgets.get_widget(
2742+ "spinbutton_slideshow_step")
2743+ spinbutton_slideshow_step.set_value(self.config.slideshow_step)
2744+
2745+ def add_to_model_and_config(self, selected_folder, model, folders, kind):
2746+ """
2747+ Add selected_folder to the model and the folders list while updating
2748+ the configuration item section specified by type
2749+ """
2750+ if not selected_folder in folders:
2751+ model.append([selected_folder])
2752+
2753+ if(folders == None):
2754+ folders = [selected_folder]
2755+ else:
2756+ folders.append(selected_folder)
2757+
2758+ if "" in folders:
2759+ folders.remove("")
2760+ str_folders = ";".join(folders)
2761+ self.config.write_content_value(kind, "folders", str_folders)
2762+
2763+ def init_model(self, model, items):
2764+ """Fill model with items from supplied list"""
2765+ for i in range(len(items)):
2766+ if not str(items[i]).strip() == "":
2767+ model.insert(i, [items[i]])
2768+
2769+ def load_themes(self):
2770+ """Load themes"""
2771+ themes = os.listdir(os.path.join(self.config.data_dir, 'themes'))
2772+ for element in themes:
2773+ theme = os.path.join(self.config.data_dir, 'themes', element)
2774+ if os.path.isdir(theme):
2775+ self.themes.append(element)
2776+
2777+
2778+class LogViewer:
2779+ """
2780+ Implements dialog that allows user to see logged events.
2781+
2782+ This dialog is used to check Entertainer logfiles. It reads all data from
2783+ selected file and saves rows to self.log_rows. Then it filters unwanted
2784+ rows away by calling self.filterMessages(). This method adds rows to
2785+ ListStore, which is the model of TreeView object.
2786+
2787+ Combobox and refresh -button actions read files again
2788+ Checkbox actions just filter current rows again
2789+ """
2790+
2791+ GLADE_DIR = os.path.join(os.path.dirname(__file__), "glade")
2792+
2793+ # Is this dialog running as a stand alone process
2794+ __STAND_ALONE = None
2795+
2796+ widgets = None
2797+ dialog = None
2798+ log_store = None
2799+ log_rows = []
2800+
2801+ gladefile = os.path.join(GLADE_DIR, "log_dialog.glade")
2802+
2803+ def __init__(self, stand_alone):
2804+ self.logfile_entertainer = Configuration().LOG
2805+ self.logger = Logger().getLogger('utils.log_viewer')
2806+
2807+ self.__STAND_ALONE = stand_alone
2808+ try:
2809+ self.widgets = gtk.glade.XML(self.gladefile)
2810+ except RuntimeError:
2811+ self.logger.critical("Couldn't open glade file: " + self.gladefile)
2812+ sys.exit(1)
2813+ callback_dic = {
2814+ "on_close_log_button_clicked" : self.on_close_log_button_clicked,
2815+ "on_log_refresh_button_clicked" : self.update_log_rows,
2816+ "on_checkbutton_debug_toggled" : self.filter_messages,
2817+ "on_checkbutton_critical_toggled" : self.filter_messages,
2818+ "on_checkbutton_error_toggled" : self.filter_messages,
2819+ "on_checkbutton_warning_toggled" : self.filter_messages,
2820+ "on_checkbutton_info_toggled" : self.filter_messages }
2821+
2822+ self.widgets.signal_autoconnect(callback_dic)
2823+
2824+ # Create log treeview
2825+ treeview = self.widgets.get_widget("treeview_log")
2826+ cell_renderer1 = gtk.CellRendererText()
2827+ cell_renderer2 = gtk.CellRendererText()
2828+ cell_renderer3 = gtk.CellRendererText()
2829+ cell_renderer4 = gtk.CellRendererText()
2830+
2831+ column1 = gtk.TreeViewColumn("Date")
2832+ column1.pack_start(cell_renderer1, True)
2833+ column1.set_attributes(cell_renderer1, text = 0)
2834+
2835+ column2 = gtk.TreeViewColumn("Time")
2836+ column2.pack_start(cell_renderer2, True)
2837+ column2.set_attributes(cell_renderer2, text = 1)
2838+
2839+ column3 = gtk.TreeViewColumn("Type")
2840+ column3.pack_start(cell_renderer3, True)
2841+ column3.set_attributes(cell_renderer3, text = 2)
2842+
2843+ column4 = gtk.TreeViewColumn("Message")
2844+ column4.pack_end(cell_renderer4, True)
2845+ column4.set_attributes(cell_renderer4, text = 3)
2846+
2847+ treeview.append_column(column1)
2848+ treeview.append_column(column2)
2849+ treeview.append_column(column3)
2850+ treeview.append_column(column4)
2851+ treeview.set_headers_visible(True)
2852+
2853+ # Set model to view and read data from logfile
2854+ self.log_store = gtk.ListStore(str, str, str, str)
2855+ treeview.set_model(self.log_store)
2856+ self.update_log_rows()
2857+
2858+ # Show Log viewer dialog
2859+ self.dialog = self.widgets.get_widget("LogDialog")
2860+ self.dialog.resize(750, 500)
2861+ self.dialog.connect("destroy", self.on_close_log_button_clicked)
2862+ self.dialog.show()
2863+
2864+ def update_log_rows(self, widget=None):
2865+ """Read logfile and udpate treeview"""
2866+ self.log_rows[:] = []
2867+
2868+ try:
2869+ for line in open(self.logfile_entertainer, 'r'):
2870+ try:
2871+ line_table = line.split()
2872+ message = ' '.join(line_table[3:])
2873+ row = line_table[:3] + [message]
2874+ parsed_row = parse_row(row)
2875+ self.log_rows.append(parsed_row)
2876+ except IndexError:
2877+ print "Cannot parse log line: ", line
2878+ except IOError:
2879+ print "Cannot find logfile: ", self.logfile_entertainer
2880+
2881+ # Reverse so that the latest message is at top
2882+ self.log_rows.reverse()
2883+ # Filter unwated message types
2884+ self.filter_messages()
2885+
2886+ def filter_messages(self, widget = None):
2887+ """Checks which message types should be displayed on treeview"""
2888+ if self.log_store:
2889+ self.log_store.clear()
2890+
2891+ debug = self.widgets.get_widget("checkbutton_debug").get_active()
2892+ critical = self.widgets.get_widget("checkbutton_critical").get_active()
2893+ error = self.widgets.get_widget("checkbutton_error").get_active()
2894+ warning = self.widgets.get_widget("checkbutton_warning").get_active()
2895+ info = self.widgets.get_widget("checkbutton_info").get_active()
2896+
2897+ for element in self.log_rows:
2898+ if element[2] == "DEBUG" and debug:
2899+ self.log_store.append(element)
2900+ elif element[2] == "CRITICAL" and critical:
2901+ self.log_store.append(element)
2902+ elif element[2] == "ERROR" and error:
2903+ self.log_store.append(element)
2904+ elif element[2] == "WARNING" and warning:
2905+ self.log_store.append(element)
2906+ elif element[2] == "INFO" and info:
2907+ self.log_store.append(element)
2908+
2909+ # Signal handlers
2910+ def on_close_log_button_clicked(self, widget):
2911+ """
2912+ If running as a stand alone process, quit.
2913+ Otherwise only destroy dialog.
2914+ """
2915+ self.dialog.hide()
2916+ self.dialog.destroy()
2917+ if(self.__STAND_ALONE):
2918+ gtk.main_quit()
2919+
2920+
2921+def parse_row(row):
2922+ """
2923+ This parses the input list into a list suitable for the logviewer
2924+ @author Joshua Scotton
2925+ @param row The input list [Date, Time, Class, Type + Description]
2926+ """
2927+ if row[3][:5] == "DEBUG":
2928+ return [row[0], row[1], "DEBUG",
2929+ row[2] + ": " + row[3][5:]]
2930+ elif row[3][:8] == "CRITICAL":
2931+ return [row[0], row[1], "CRITICAL",
2932+ row[2] + ": " + row[3][8:]]
2933+ elif row[3][:5] == "ERROR":
2934+ return [row[0], row[1], "ERROR",
2935+ row[2] + ": " + row[3][5:]]
2936+ elif row[3][:7] == "WARNING":
2937+ return [row[0], row[1], "WARNING",
2938+ row[2] + ": " + row[3][7:]]
2939+ elif row[3][:4] == "INFO":
2940+ return [row[0], row[1], "INFO",
2941+ row[2] + ": " + row[3][4:]]
2942+
2943+
2944+class OpenFeedSourceDialog:
2945+ '''Feed source reader dialog'''
2946+
2947+ GLADE_DIR = os.path.join(os.path.dirname(__file__), "glade")
2948+
2949+ widgets = None
2950+ dialog = None
2951+ tree_widget = None
2952+ feeds = None
2953+ url = None
2954+
2955+ def __init__(self, the_widget, the_feeds):
2956+ """initialises the gtk window and displays it"""
2957+
2958+ #feeds is a pointer to a list of feeds from the config file
2959+ self.feeds = the_feeds
2960+
2961+ #needed so we can add feeds to the feed list widget
2962+ self.tree_widget = the_widget
2963+
2964+ # Load glade UI
2965+ self.gladefile = os.path.join(self.GLADE_DIR,
2966+ "open_feed_source_dialog.glade")
2967+ self.widgets = gtk.glade.XML(self.gladefile)
2968+
2969+ # Get content management dialog and bind signal callbacks
2970+ self.dialog = self.widgets.get_widget("open_source_dialog")
2971+ if (self.dialog):
2972+ callback_dic = {
2973+ "on_fileOpen_clicked" : self.on_fileOpen_clicked,
2974+ "on_lifereaButton_clicked" : self.on_lifereaButton_clicked,
2975+ "on_enterURL_clicked" : self.on_enterURL_clicked,
2976+ "on_closeButton_clicked" : self.on_closeButton_clicked,
2977+ "on_url_dialog_ok_button_clicked" :
2978+ self.on_url_dialog_ok_button_clicked,
2979+ "on_url_dialog_cancel_button_clicked" :
2980+ self.on_url_dialog_cancel_button_clicked,
2981+ "on_url_dialog_delete_event" : self.on_url_dialog_delete_event
2982+ }
2983+
2984+ self.widgets.signal_autoconnect(callback_dic)
2985+
2986+ # Initilize dialog widgets with correct values and show dialog
2987+ self.dialog.resize(300, 200)
2988+ self.dialog.run()
2989+
2990+ def on_fileOpen_clicked(self, widget):
2991+ """Opens add file dialog and then adds all feeds in the opml file
2992+ selected """
2993+
2994+ #get the model for the feed list widget so we can add the new feeds
2995+ model = self.tree_widget.get_model()
2996+
2997+ #create select file dialog
2998+ dialog = gtk.FileChooserDialog(_("Select OPML file"), None,
2999+ gtk.FILE_CHOOSER_ACTION_OPEN,
3000+ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
3001+ gtk.RESPONSE_OK))
3002+
3003+ #set dialog up to filter for only opml files
3004+ file_filter = gtk.FileFilter()
3005+ file_filter.set_name(_("OPML files"))
3006+ file_filter.add_pattern("*.opml")
3007+ dialog.add_filter(file_filter)
3008+
3009+ #set dialog up to allow multiple selections
3010+ dialog.set_select_multiple(True)
3011+
3012+ #run the dialog
3013+ status = dialog.run()
3014+
3015+ # if file was selected, get list of feeds from it and add to
3016+ #model and update config file
3017+ if(status == gtk.RESPONSE_OK):
3018+ FeedConfigTools().add_file_feeds_to_widget(
3019+ dialog.get_filenames(), model, self.feeds)
3020+ dialog.destroy()
3021+
3022+ def on_lifereaButton_clicked(self, widget):
3023+ """Adds any liferea feeds it finds to the feed widget and config file
3024+ """
3025+ #get the model
3026+ model = self.tree_widget.get_model()
3027+ #get the liferea feeds and then send everything to the
3028+ #add_file_feeds_to_widget method
3029+ FeedConfigTools().add_file_feeds_to_widget(
3030+ [OPMLParser().get_liferea_opml()], model, self.feeds)
3031+
3032+ def on_enterURL_clicked(self, widget):
3033+ """gets a opml file link from a user and adds any feeds it finds to the
3034+ feed widget and config file"""
3035+ url_dialog = self.widgets.get_widget("url_dialog")
3036+ model = self.tree_widget.get_model()
3037+ # Open dialog
3038+ url_dialog.set_title(_("Add OPML File"))
3039+ status = url_dialog.run()
3040+ # If folder was selected we add it to model and update config file
3041+ if(status == gtk.RESPONSE_OK):
3042+ FeedConfigTools().add_file_feeds_to_widget([self.url],
3043+ model, self.feeds)
3044+
3045+ def on_closeButton_clicked(self, widget):
3046+ self.dialog.hide()
3047+ self.dialog.destroy()
3048+
3049+ def on_url_dialog_ok_button_clicked(self, widget):
3050+ """URL dialog OK button pressed. Sets self.url"""
3051+ url_dialog = self.widgets.get_widget("url_dialog")
3052+ url_entry = self.widgets.get_widget("url_entry")
3053+ url_dialog.hide()
3054+ self.url = url_entry.get_text()
3055+ url_entry.set_text("")
3056+ url_dialog.response(gtk.RESPONSE_OK)
3057+
3058+ def on_url_dialog_cancel_button_clicked(self, widget):
3059+ """URL dialog cancelled. Hides dialog"""
3060+ url_dialog = self.widgets.get_widget("url_dialog")
3061+ url_entry = self.widgets.get_widget("url_entry")
3062+ url_dialog.hide()
3063+ url_entry.set_text("")
3064+ url_dialog.response(gtk.RESPONSE_CANCEL)
3065+
3066+ def on_url_dialog_delete_event(self, widget, data):
3067+ """Dialog's X clicked. Hides dialog"""
3068+ url_dialog = self.widgets.get_widget("url_dialog")
3069+ url_entry = self.widgets.get_widget("url_entry")
3070+ url_dialog.hide()
3071+ url_entry.set_text("")
3072+ url_dialog.response(gtk.RESPONSE_CANCEL)
3073+ return True
3074+
3075+
3076
3077=== added file 'entertainerlib/download.py'
3078--- entertainerlib/download.py 1970-01-01 00:00:00 +0000
3079+++ entertainerlib/download.py 2009-07-14 04:24:07 +0000
3080@@ -0,0 +1,455 @@
3081+# Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
3082+'''Downloader classes.'''
3083+
3084+import locale
3085+import os
3086+import re
3087+import socket
3088+import threading
3089+import urllib
3090+from xml.dom import minidom
3091+
3092+import gobject
3093+
3094+# Amazon licence for Entertainer
3095+LICENSE_KEY = "1YCWYZ0SPPAJ27YZZ482"
3096+DEFAULT_LOCALE = "en_US"
3097+ASSOCIATE = "webservices-20"
3098+
3099+# We are not allowed to batch more than 2 requests at once
3100+# http://docs.amazonwebservices.com/AWSEcommerceService/4-0/
3101+# PgCombiningOperations.html
3102+MAX_BATCH_JOBS = 2
3103+
3104+
3105+class AlbumArtDownloader(threading.Thread):
3106+ """
3107+ Search and download album art from the internet.
3108+
3109+ This class is heavily based on Rhythmbox - AlbumArt plugin's class
3110+ 'AmazonCoverArtSearch'. That plugin is released under GPLv2 (or higher)
3111+ and it's copyrights belong to Gareth Murphy and Martin Szulecki.
3112+
3113+ See more: http://www.gnome.org/projects/rhythmbox/
3114+
3115+ If you want better cover search, please contribute to Rhyhtmbox project.
3116+ """
3117+
3118+ def __init__(self, album, artist, art_file_path, callback = None):
3119+ """
3120+ Initialize album art downloader
3121+ @param album: Album title
3122+ @param artist: Artist name
3123+ @param art_file_path: Path where albumart is saved
3124+ @param callback: Callback function that is called after search if set
3125+ """
3126+ threading.Thread.__init__(self)
3127+ self.setName("AlbumArt Downloader")
3128+ self.callback_function = callback # Callback function
3129+ self.album = album # Album title
3130+ self.artist = artist # Artist name
3131+ # Album art files are in this directory
3132+ self.path = art_file_path
3133+ (self.tld, self.encoding) = self.__get_locale ()
3134+
3135+ def run(self):
3136+ """Start searching and downloading albumart."""
3137+ self.search()
3138+
3139+ def __get_locale (self):
3140+ '''Get locale information from user\'s machine'''
3141+ # "JP is the only locale that correctly takes UTF8 input.
3142+ # All other locales use LATIN1."
3143+ # http://developer.amazonwebservices.com/connect/
3144+ # entry.jspa?externalID=1295&categoryID=117
3145+ supported_locales = {
3146+ "en_US" : ("com", "latin1"),
3147+ "en_GB" : ("co.uk", "latin1"),
3148+ "de" : ("de", "latin1"),
3149+ "ja" : ("jp", "utf8")
3150+ }
3151+
3152+ lc_id = DEFAULT_LOCALE
3153+ default = locale.getdefaultlocale ()[0]
3154+ if default:
3155+ if supported_locales.has_key (default):
3156+ lc_id = default
3157+ else:
3158+ lang = default.split("_")[0]
3159+ if supported_locales.has_key (lang):
3160+ lc_id = lang
3161+
3162+ return supported_locales[lc_id]
3163+
3164+ def __valid_match (self, item):
3165+ '''Determine if item matches tag criteria'''
3166+ return (hasattr (item, "LargeImage") or hasattr (item, "MediumImage")) \
3167+ and hasattr (item, "ItemAttributes")
3168+
3169+ def __tidy_up_string (self, str_input):
3170+ """
3171+ Tidy up string. Remove spaces, convert to lowercase and replace chars.
3172+ """
3173+ # Lowercase
3174+ str_input = str_input.lower ()
3175+ # Strip
3176+ str_input = str_input.strip ()
3177+
3178+ # TODO: Convert accented to unaccented
3179+ str_input = str_input.replace (" - ", " ")
3180+ str_input = str_input.replace (": ", " ")
3181+ str_input = str_input.replace (" & ", " and ")
3182+
3183+ return str_input
3184+
3185+ def search(self):
3186+ """Search album art from Amazon"""
3187+ self.searching = True
3188+ self.cancel = False
3189+ self.keywords = []
3190+
3191+ st_artist = self.artist or u'Unknown'
3192+ st_album = self.album or u'Unknown'
3193+
3194+ if st_artist == st_album == u'Unknown':
3195+ self.on_search_completed (None)
3196+ return
3197+
3198+ # Tidy up
3199+
3200+ # Replace quote characters
3201+ # don't replace single quote: could be important punctuation
3202+ for char in ["\""]:
3203+ st_artist = st_artist.replace (char, '')
3204+ st_album = st_album.replace (char, '')
3205+
3206+
3207+ self.st_album = st_album
3208+ self.st_artist = st_artist
3209+
3210+ # Remove variants of Disc/CD [1-9] from album title before search
3211+ for exp in ["\([Dd]isc *[1-9]+\)", "\([Cc][Dd] *[1-9]+\)"]:
3212+ p = re.compile (exp)
3213+ st_album = p.sub ('', st_album)
3214+
3215+ st_album_no_vol = st_album
3216+ for exp in ["\(*[Vv]ol.*[1-9]+\)*"]:
3217+ p = re.compile (exp)
3218+ st_album_no_vol = p.sub ('', st_album_no_vol)
3219+
3220+ self.st_album_no_vol = st_album_no_vol
3221+
3222+ # Save current search's entry properties
3223+ self.search_album = st_album
3224+ self.search_artist = st_artist
3225+ self.search_album_no_vol = st_album_no_vol
3226+
3227+ # TODO: Improve to decrease wrong cover downloads, maybe add severity?
3228+ # Assemble list of search keywords (and thus search queries)
3229+ if st_album == u'Unknown':
3230+ self.keywords.append ("%s Best of" % (st_artist))
3231+ self.keywords.append ("%s Greatest Hits" % (st_artist))
3232+ self.keywords.append ("%s Essential" % (st_artist))
3233+ self.keywords.append ("%s Collection" % (st_artist))
3234+ self.keywords.append ("%s" % (st_artist))
3235+ elif st_artist == u'Unknown':
3236+ self.keywords.append ("%s" % (st_album))
3237+ if st_album_no_vol != st_artist:
3238+ self.keywords.append ("%s" % (st_album_no_vol))
3239+ self.keywords.append ("Various %s" % (st_album))
3240+ else:
3241+ if st_album != st_artist:
3242+ self.keywords.append ("%s %s" % (st_artist, st_album))
3243+ if st_album_no_vol != st_album:
3244+ self.keywords.append ("%s %s" %
3245+ (st_artist, st_album_no_vol))
3246+ self.keywords.append ("Various %s" % (st_album))
3247+ self.keywords.append ("%s" % (st_artist))
3248+
3249+ # Initiate asynchronous search
3250+ self.search_next ()
3251+
3252+ def search_next(self):
3253+ """Search again, because the last one didn't find any covers."""
3254+ if len (self.keywords) == 0:
3255+ # No keywords left to search -> no results
3256+ self.on_search_completed (None)
3257+ return False
3258+
3259+ self.searching = True
3260+
3261+ url = "http://ecs.amazonaws." + self.tld + "/onca/xml" \
3262+ "?Service=AWSECommerceService" \
3263+ "&AWSAccessKeyId=" + LICENSE_KEY + \
3264+ "&AssociateTag=" + ASSOCIATE + \
3265+ "&ResponseGroup=Images,ItemAttributes" \
3266+ "&Operation=ItemSearch" \
3267+ "&ItemSearch.Shared.SearchIndex=Music"
3268+
3269+ job = 1
3270+ while job <= MAX_BATCH_JOBS and len (self.keywords) > 0:
3271+ keyword = self.keywords.pop (0)
3272+ keyword = keyword.encode (self.encoding, "ignore")
3273+ keyword = keyword.strip ()
3274+ keyword = urllib.quote (keyword)
3275+ url += "&ItemSearch.%d.Keywords=%s" % (job, keyword)
3276+ job += 1
3277+
3278+ # Retrieve search for keyword
3279+ temp = urllib.urlopen(url)
3280+ search_results = temp.read()
3281+ self.on_search_response(search_results)
3282+ return True
3283+
3284+ def __unmarshal(self, element):
3285+ rc = object()
3286+ child_elements = [e for e in element.childNodes if isinstance (e,
3287+ minidom.Element)]
3288+ if child_elements:
3289+ for child in child_elements:
3290+ key = child.tagName
3291+ if hasattr (rc, key):
3292+ if not isinstance (getattr (rc, key), list):
3293+ setattr (rc, key, [getattr (rc, key)])
3294+ getattr (rc, key).append (self.__unmarshal (child))
3295+ # get_best_match_urls() wants a list, even if there is only
3296+ # one item/artist
3297+ elif child.tagName in ("Items", "Item", "Artist"):
3298+ setattr (rc, key, [self.__unmarshal(child)])
3299+ else:
3300+ setattr (rc, key, self.__unmarshal(child))
3301+ else:
3302+ rc = "".join ([e.data for e in element.childNodes if isinstance (e,
3303+ minidom.Text)])
3304+ return rc
3305+
3306+ def on_search_response (self, result_data):
3307+ '''Check search results
3308+
3309+ If results are not good, we search again with the next keyword.
3310+ '''
3311+ if result_data is None:
3312+ self.search_next()
3313+ return
3314+
3315+ try:
3316+ xmldoc = minidom.parseString(result_data)
3317+ except (TypeError, AttributeError):
3318+ self.search_next()
3319+ return
3320+
3321+ data = self.__unmarshal (xmldoc)
3322+ if not hasattr (data, "ItemSearchResponse") or \
3323+ not hasattr (data.ItemSearchResponse, "Items"):
3324+ # Something went wrong ...
3325+ self.search_next ()
3326+ else:
3327+ # We got some search results
3328+ self.on_search_results (data.ItemSearchResponse.Items)
3329+
3330+ def on_search_results (self, results):
3331+ '''Results were found, now we need to take action.'''
3332+ self.on_search_completed (results)
3333+
3334+ def on_search_completed (self, result):
3335+ """
3336+ Search completed and results found.
3337+
3338+ Download large album art image from the first result and save it to
3339+ the disk. This function diverges greatly from the rhythmbox
3340+ implementation in order to avoid their loader and CoverArtDatabase
3341+ """
3342+ self.searching = False
3343+ image_urls = self.get_best_match_urls(result)
3344+ if len(image_urls) == 0:
3345+ return
3346+ image_url = image_urls[0]
3347+ image_file = urllib.urlopen(image_url)
3348+ # base64 encode artist and album so there can be a '/' in the artist
3349+ # or album
3350+ artist_album = self.artist + " - " + self.album
3351+ artist_album = artist_album.encode("base64")
3352+
3353+ dest = open(os.path.join(self.path, artist_album + ".jpg"),'w')
3354+ dest.write(image_file.read())
3355+ dest.close()
3356+
3357+ if self.callback_function is not None:
3358+ self.callback_function(self.artist, self.album)
3359+
3360+ def get_best_match_urls (self, search_results):
3361+ """Return tuple of URL's to large and medium cover of the best match"""
3362+ # Default to "no match", our results must match our criteria
3363+
3364+ # This code comes from Rhythmbox so we can't control the use of 'filter'
3365+ # pylint: disable-msg=W0141
3366+
3367+ if not search_results:
3368+ return []
3369+
3370+ best_match = None
3371+
3372+ for result in search_results:
3373+ if not hasattr (result, "Item"):
3374+ # Search was unsuccessful, try next batch job
3375+ continue
3376+
3377+ items = filter(self.__valid_match, result.Item)
3378+ if self.search_album != u'Unknown':
3379+ album_check = self.__tidy_up_string (self.search_album)
3380+ for item in items:
3381+ if not hasattr (item.ItemAttributes, "Title"):
3382+ continue
3383+
3384+ album = self.__tidy_up_string (item.ItemAttributes.Title)
3385+ if album == album_check:
3386+ # Found exact album, can not get better than that
3387+ best_match = item
3388+ break
3389+ # If we already found a best_match, just keep checking for
3390+ # exact one. Check the results for both an album name that
3391+ # contains the name we're searching for, and an album name
3392+ # that's a substring of the name we're searching for
3393+ elif (best_match is None) and \
3394+ (album.find (album_check) != -1 or
3395+ album_check.find (album) != -1):
3396+ best_match = item
3397+
3398+ # If we still have no definite hit, use first result where artist
3399+ # matches
3400+ if (self.search_album == u'Unknown' and \
3401+ self.search_artist != u'Unknown'):
3402+ artist_check = self.__tidy_up_string (self.search_artist)
3403+ if best_match is None:
3404+ # Check if artist appears in the Artists list
3405+ hit = False
3406+ for item in items:
3407+ if not hasattr (item.ItemAttributes, "Artist"):
3408+ continue
3409+
3410+ for artist in item.ItemAttributes.Artist:
3411+ artist = self.__tidy_up_string (artist)
3412+ if artist.find (artist_check) != -1:
3413+ best_match = item
3414+ hit = True
3415+ break
3416+ if hit:
3417+ break
3418+
3419+ urls = [getattr (best_match, size).URL for size in ("LargeImage",
3420+ "MediumImage")
3421+ if hasattr (best_match, size)]
3422+ if urls:
3423+ return urls
3424+
3425+ # No search was successful
3426+ return []
3427+
3428+
3429+class LyricsDownloader(object):
3430+ """
3431+ Search and download song lyrics from the internet.
3432+ Update music cache if lyrics found.
3433+ """
3434+
3435+ # The permanent user ID from Lyricsfly
3436+ # NOTICE: This is the personal user ID for Entertainer, if you want to
3437+ # experiment with the API from lyricsfly you can get an ID here =>
3438+ # http://lyricsfly.com/api/, don't use this one as abuse of our key may
3439+ # invalidate it.
3440+ _LYRICSFLY_KEY = 'YzIxOTM4M2NkNGQ4MmRmODEtZW50ZXJ0YWluZXItcHJvamVjdC5jb20='
3441+
3442+ def __init__(self, title, artist, callback):
3443+ """
3444+ Initialize lyrics downloader.
3445+ @param title: Title of the track
3446+ @param artist: Artist of the track
3447+ @param callback: Callback function which is called when search is over.
3448+ lyrics are given as a parameter to this callback function.
3449+ """
3450+ self.title = title.lower()
3451+ self.artist = artist.lower()
3452+ self.callback = callback
3453+
3454+ def search(self):
3455+ """
3456+ Search lyrics and download if found. Search is done asynchronously.
3457+ This method returns immediately and set callback is called when search
3458+ is over.
3459+ """
3460+ gobject.timeout_add(2000, self._async_search)
3461+
3462+ def _async_search(self):
3463+ """
3464+ Search lyrics and download if found
3465+ """
3466+ lyrics = ""
3467+ self._clean_up_artist_title()
3468+ lyrics_xml = self._get_lyrics_xml()
3469+ if lyrics_xml is not None:
3470+ lyrics = self._parse_lyrics_xml(lyrics_xml)
3471+ self.callback(lyrics)
3472+
3473+ def _clean_up_artist_title(self):
3474+ """
3475+ Clean up the artist and title.
3476+ """
3477+ # Clean spaces
3478+ self.title = self.title.strip()
3479+ self.artist = self.artist.strip()
3480+
3481+ # Convert title and artist to use in url, special symbols have to be
3482+ # replaced by a '%' not '%xx'
3483+ # TODO: Find out what the special symbols are (', &, ...)
3484+ # not letters, digits, spaces and ()$^*=:;|#@}{][!,.-_\
3485+ self.artist = urllib.quote(self.artist.encode('utf-8'),
3486+ "'&()$^*=:;|#@}{][!,\\")
3487+ self.title = urllib.quote(self.title.encode('utf-8'),
3488+ "'&()$^*=:;|#@}{][!,\\")
3489+
3490+ self.artist = self.artist.replace("'", "%").replace("&", "%")
3491+ self.title = self.title.replace("'", "%").replace("&", "%")
3492+
3493+ def _get_lyrics_xml(self):
3494+ """
3495+ Download the lyrics XML-file.
3496+ """
3497+ lyrics_xml = None
3498+
3499+ # timeout in seconds
3500+ timeout = 5
3501+ socket.setdefaulttimeout(timeout)
3502+
3503+ url = "http://lyricsfly.com/api/api.php?i=%s&a=%s&t=%s" \
3504+ % (self._LYRICSFLY_KEY.decode('base64'), self.artist, self.title)
3505+ try:
3506+ temp = urllib.urlopen(url)
3507+ lyrics_xml = temp.read()
3508+ except IOError:
3509+ return None
3510+
3511+ return lyrics_xml
3512+
3513+ def _parse_lyrics_xml(self, lyrics_xml):
3514+ """Parse lyrics XML and return lyrics string"""
3515+ xmldoc = minidom.parseString(lyrics_xml).documentElement
3516+
3517+ # Get the lyric from the XML file
3518+ try:
3519+ lyrics = xmldoc.getElementsByTagName('tx')[0].firstChild.nodeValue
3520+ except IndexError:
3521+ return ''
3522+
3523+ # Clean spaces and enters
3524+ lyrics = lyrics.strip().replace('\n', '')
3525+ lyrics = lyrics.replace('[br]', '\n')
3526+
3527+ # Add the artist and title to the top of the lyric
3528+ lyrics = xmldoc.getElementsByTagName('ar')[0].firstChild.nodeValue + \
3529+ ' - ' + xmldoc.getElementsByTagName('tt')[0].firstChild.nodeValue + \
3530+ '\n\n' + lyrics
3531+
3532+ xmldoc.unlink()
3533+
3534+ return lyrics
3535+
3536
3537=== renamed directory 'entertainerlib/utils/glade' => 'entertainerlib/glade'
3538=== renamed file 'entertainerlib/utils/glade/entertainer-content-management.glade' => 'entertainerlib/glade/manager.glade'
3539--- entertainerlib/utils/glade/entertainer-content-management.glade 2009-05-31 16:36:18 +0000
3540+++ entertainerlib/glade/manager.glade 2009-08-23 01:01:18 +0000
3541@@ -1,1481 +1,2170 @@
3542-<?xml version="1.0"?>
3543+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
3544+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
3545+
3546 <glade-interface>
3547- <!-- interface-requires gtk+ 2.16 -->
3548- <!-- interface-naming-policy toplevel-contextual -->
3549- <widget class="GtkDialog" id="ContentManagementDialog">
3550- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
3551- <property name="border_width">5</property>
3552- <property name="title" translatable="yes">Content management</property>
3553- <property name="modal">True</property>
3554- <property name="window_position">center</property>
3555- <property name="icon_name">applications-multimedia</property>
3556- <property name="type_hint">dialog</property>
3557- <property name="has_separator">False</property>
3558- <signal name="destroy" handler="on_ContentManagementDialog_destroy"/>
3559- <child internal-child="vbox">
3560- <widget class="GtkVBox" id="dialog-layout">
3561- <property name="visible">True</property>
3562- <property name="spacing">2</property>
3563- <child>
3564- <widget class="GtkNotebook" id="tabs">
3565- <property name="visible">True</property>
3566- <property name="can_focus">True</property>
3567- <property name="has_focus">True</property>
3568- <property name="can_default">True</property>
3569- <property name="has_default">True</property>
3570- <child>
3571- <widget class="GtkVBox" id="vbox_videos">
3572- <property name="visible">True</property>
3573- <child>
3574- <widget class="GtkVBox" id="vbox1">
3575- <property name="visible">True</property>
3576- <child>
3577- <widget class="GtkFrame" id="frame_video_lib1">
3578- <property name="visible">True</property>
3579- <property name="border_width">5</property>
3580- <property name="label_xalign">0</property>
3581- <property name="shadow_type">none</property>
3582- <child>
3583- <widget class="GtkAlignment" id="alignment1">
3584- <property name="visible">True</property>
3585- <property name="left_padding">12</property>
3586- <child>
3587- <widget class="GtkVBox" id="vbox2">
3588- <property name="visible">True</property>
3589- <child>
3590- <widget class="GtkLabel" id="label_video_folder_tip1">
3591- <property name="visible">True</property>
3592- <property name="xalign">0</property>
3593- <property name="label" translatable="yes">Entertainer video library scans data from the listed folders.</property>
3594- <property name="single_line_mode">True</property>
3595- </widget>
3596- <packing>
3597- <property name="expand">False</property>
3598- <property name="padding">5</property>
3599- <property name="position">0</property>
3600- </packing>
3601- </child>
3602- <child>
3603- <widget class="GtkHBox" id="hbox2">
3604- <property name="visible">True</property>
3605- <child>
3606- <widget class="GtkScrolledWindow" id="scrolledwindow1">
3607- <property name="visible">True</property>
3608- <property name="can_focus">True</property>
3609- <property name="hscrollbar_policy">automatic</property>
3610- <property name="vscrollbar_policy">automatic</property>
3611- <property name="shadow_type">in</property>
3612- <child>
3613- <widget class="GtkTreeView" id="treeview_videos">
3614- <property name="visible">True</property>
3615- <property name="can_focus">True</property>
3616- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
3617- <property name="enable_search">False</property>
3618- </widget>
3619- </child>
3620- </widget>
3621- <packing>
3622- <property name="position">0</property>
3623- </packing>
3624- </child>
3625- <child>
3626- <widget class="GtkVButtonBox" id="vbuttonbox1">
3627- <property name="visible">True</property>
3628- <property name="spacing">5</property>
3629- <property name="layout_style">start</property>
3630- <child>
3631- <widget class="GtkButton" id="button_add_videos">
3632- <property name="label">gtk-add</property>
3633- <property name="visible">True</property>
3634- <property name="can_focus">True</property>
3635- <property name="has_focus">True</property>
3636- <property name="can_default">True</property>
3637- <property name="has_default">True</property>
3638- <property name="receives_default">False</property>
3639- <property name="use_stock">True</property>
3640- <signal name="clicked" handler="on_button_add_videos_clicked"/>
3641- </widget>
3642- <packing>
3643- <property name="expand">False</property>
3644- <property name="fill">False</property>
3645- <property name="position">0</property>
3646- </packing>
3647- </child>
3648- <child>
3649- <widget class="GtkButton" id="button_remove_videos">
3650- <property name="label">gtk-remove</property>
3651- <property name="visible">True</property>
3652- <property name="can_focus">True</property>
3653- <property name="receives_default">False</property>
3654- <property name="use_stock">True</property>
3655- <signal name="clicked" handler="on_button_remove_videos_clicked"/>
3656- </widget>
3657- <packing>
3658- <property name="expand">False</property>
3659- <property name="fill">False</property>
3660- <property name="position">1</property>
3661- </packing>
3662- </child>
3663- <child>
3664- <widget class="GtkButton" id="button_edit_videos">
3665- <property name="label">gtk-edit</property>
3666- <property name="visible">True</property>
3667- <property name="can_focus">True</property>
3668- <property name="receives_default">False</property>
3669- <property name="use_stock">True</property>
3670- <signal name="clicked" handler="on_button_edit_videos_clicked"/>
3671- </widget>
3672- <packing>
3673- <property name="expand">False</property>
3674- <property name="fill">False</property>
3675- <property name="position">2</property>
3676- </packing>
3677- </child>
3678- </widget>
3679- <packing>
3680- <property name="expand">False</property>
3681- <property name="padding">5</property>
3682- <property name="position">1</property>
3683- </packing>
3684- </child>
3685- </widget>
3686- <packing>
3687- <property name="position">1</property>
3688- </packing>
3689- </child>
3690- </widget>
3691- </child>
3692- </widget>
3693- </child>
3694- <child>
3695- <widget class="GtkLabel" id="label_video_library1">
3696- <property name="visible">True</property>
3697- <property name="label" translatable="yes">&lt;b&gt;Video library&lt;/b&gt;</property>
3698- <property name="use_markup">True</property>
3699- </widget>
3700- <packing>
3701- <property name="type">label_item</property>
3702- </packing>
3703- </child>
3704- </widget>
3705- <packing>
3706- <property name="position">0</property>
3707- </packing>
3708- </child>
3709- <child>
3710- <widget class="GtkFrame" id="frame1">
3711- <property name="visible">True</property>
3712- <property name="label_xalign">0</property>
3713- <property name="shadow_type">none</property>
3714- <child>
3715- <widget class="GtkAlignment" id="alignment2">
3716- <property name="visible">True</property>
3717- <property name="left_padding">12</property>
3718- <child>
3719- <widget class="GtkCheckButton" id="video_metadata_checkbox">
3720- <property name="label" translatable="yes">Download movie and TV-series metadata</property>
3721- <property name="visible">True</property>
3722- <property name="can_focus">True</property>
3723- <property name="receives_default">False</property>
3724- <property name="use_underline">True</property>
3725- <property name="draw_indicator">True</property>
3726- <signal name="toggled" handler="on_checkbutton_video_metadata_toggled"/>
3727- </widget>
3728- </child>
3729- </widget>
3730- </child>
3731- <child>
3732- <widget class="GtkLabel" id="label2">
3733- <property name="visible">True</property>
3734- <property name="label" translatable="yes">&lt;b&gt;Video library settings&lt;/b&gt;</property>
3735- <property name="use_markup">True</property>
3736- </widget>
3737- <packing>
3738- <property name="type">label_item</property>
3739- </packing>
3740- </child>
3741- </widget>
3742- <packing>
3743- <property name="position">1</property>
3744- </packing>
3745- </child>
3746- </widget>
3747- <packing>
3748- <property name="position">0</property>
3749- </packing>
3750- </child>
3751- </widget>
3752- </child>
3753- <child>
3754- <widget class="GtkLabel" id="label_video3">
3755- <property name="visible">True</property>
3756- <property name="can_focus">True</property>
3757- <property name="has_focus">True</property>
3758- <property name="has_default">True</property>
3759- <property name="label" translatable="yes">Videos</property>
3760- </widget>
3761- <packing>
3762- <property name="tab_fill">False</property>
3763- <property name="type">tab</property>
3764- </packing>
3765- </child>
3766- <child>
3767- <widget class="GtkVBox" id="vbox_music">
3768- <property name="visible">True</property>
3769- <child>
3770- <widget class="GtkFrame" id="frame_music_folders3">
3771- <property name="visible">True</property>
3772- <property name="border_width">5</property>
3773- <property name="label_xalign">0</property>
3774- <property name="shadow_type">none</property>
3775- <child>
3776- <widget class="GtkAlignment" id="alignment9">
3777- <property name="visible">True</property>
3778- <property name="left_padding">12</property>
3779- <child>
3780- <widget class="GtkVBox" id="vbox12">
3781- <property name="visible">True</property>
3782- <child>
3783- <widget class="GtkLabel" id="label8">
3784- <property name="visible">True</property>
3785- <property name="xalign">0</property>
3786- <property name="label" translatable="yes">Entertainer music library scans data from the listed folders.</property>
3787- </widget>
3788- <packing>
3789- <property name="expand">False</property>
3790- <property name="padding">5</property>
3791- <property name="position">0</property>
3792- </packing>
3793- </child>
3794- <child>
3795- <widget class="GtkHBox" id="hbox12">
3796- <property name="visible">True</property>
3797- <child>
3798- <widget class="GtkScrolledWindow" id="scrolledwindow5">
3799- <property name="visible">True</property>
3800- <property name="can_focus">True</property>
3801- <property name="hscrollbar_policy">automatic</property>
3802- <property name="vscrollbar_policy">automatic</property>
3803- <property name="shadow_type">in</property>
3804- <child>
3805- <widget class="GtkTreeView" id="treeview_music">
3806- <property name="visible">True</property>
3807- <property name="can_focus">True</property>
3808- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
3809- <property name="enable_search">False</property>
3810- </widget>
3811- </child>
3812- </widget>
3813- <packing>
3814- <property name="position">0</property>
3815- </packing>
3816- </child>
3817- <child>
3818- <widget class="GtkVButtonBox" id="vbuttonbox6">
3819- <property name="visible">True</property>
3820- <property name="spacing">5</property>
3821- <property name="layout_style">start</property>
3822- <child>
3823- <widget class="GtkButton" id="button_add_music">
3824- <property name="label">gtk-add</property>
3825- <property name="visible">True</property>
3826- <property name="can_focus">True</property>
3827- <property name="receives_default">False</property>
3828- <property name="use_stock">True</property>
3829- <signal name="clicked" handler="on_button_add_music_clicked"/>
3830- </widget>
3831- <packing>
3832- <property name="expand">False</property>
3833- <property name="fill">False</property>
3834- <property name="position">0</property>
3835- </packing>
3836- </child>
3837- <child>
3838- <widget class="GtkButton" id="button_remove_music">
3839- <property name="label">gtk-remove</property>
3840- <property name="visible">True</property>
3841- <property name="can_focus">True</property>
3842- <property name="receives_default">False</property>
3843- <property name="use_stock">True</property>
3844- <signal name="clicked" handler="on_button_remove_music_clicked"/>
3845- </widget>
3846- <packing>
3847- <property name="expand">False</property>
3848- <property name="fill">False</property>
3849- <property name="position">1</property>
3850- </packing>
3851- </child>
3852- <child>
3853- <widget class="GtkButton" id="button_edit_music">
3854- <property name="label">gtk-edit</property>
3855- <property name="visible">True</property>
3856- <property name="can_focus">True</property>
3857- <property name="receives_default">False</property>
3858- <property name="use_stock">True</property>
3859- <signal name="clicked" handler="on_button_edit_music_clicked"/>
3860- </widget>
3861- <packing>
3862- <property name="expand">False</property>
3863- <property name="fill">False</property>
3864- <property name="position">2</property>
3865- </packing>
3866- </child>
3867- </widget>
3868- <packing>
3869- <property name="expand">False</property>
3870- <property name="padding">5</property>
3871- <property name="position">1</property>
3872- </packing>
3873- </child>
3874- </widget>
3875- <packing>
3876- <property name="position">1</property>
3877- </packing>
3878- </child>
3879- </widget>
3880- </child>
3881- </widget>
3882- </child>
3883- <child>
3884- <widget class="GtkLabel" id="label_music_folders3">
3885- <property name="visible">True</property>
3886- <property name="label" translatable="yes">&lt;b&gt;Music library&lt;/b&gt;</property>
3887- <property name="use_markup">True</property>
3888- </widget>
3889- <packing>
3890- <property name="type">label_item</property>
3891- </packing>
3892- </child>
3893- </widget>
3894- <packing>
3895- <property name="position">0</property>
3896- </packing>
3897- </child>
3898- <child>
3899- <widget class="GtkFrame" id="frame_music_lib_settings3">
3900- <property name="visible">True</property>
3901- <property name="border_width">5</property>
3902- <property name="label_xalign">0</property>
3903- <property name="shadow_type">none</property>
3904- <child>
3905- <widget class="GtkAlignment" id="alignment10">
3906- <property name="visible">True</property>
3907- <property name="left_padding">12</property>
3908- <child>
3909- <widget class="GtkVBox" id="vbox13">
3910- <property name="visible">True</property>
3911- <property name="homogeneous">True</property>
3912- <child>
3913- <widget class="GtkCheckButton" id="art_checkbox">
3914- <property name="label" translatable="yes">Download album cover art</property>
3915- <property name="visible">True</property>
3916- <property name="can_focus">True</property>
3917- <property name="receives_default">False</property>
3918- <property name="use_underline">True</property>
3919- <property name="draw_indicator">True</property>
3920- <signal name="toggled" handler="on_art_checkbox_toggled"/>
3921- </widget>
3922- <packing>
3923- <property name="padding">5</property>
3924- <property name="position">0</property>
3925- </packing>
3926- </child>
3927- <child>
3928- <widget class="GtkCheckButton" id="lyrics_checkbox">
3929- <property name="label" translatable="yes">Download song lyrics</property>
3930- <property name="visible">True</property>
3931- <property name="can_focus">True</property>
3932- <property name="receives_default">False</property>
3933- <property name="use_underline">True</property>
3934- <property name="draw_indicator">True</property>
3935- <signal name="toggled" handler="on_lyrics_checkbox_toggled"/>
3936- </widget>
3937- <packing>
3938- <property name="padding">5</property>
3939- <property name="position">1</property>
3940- </packing>
3941- </child>
3942- </widget>
3943- </child>
3944- </widget>
3945- </child>
3946- <child>
3947- <widget class="GtkLabel" id="label9">
3948- <property name="visible">True</property>
3949- <property name="label" translatable="yes">&lt;b&gt;Music library settings&lt;/b&gt;</property>
3950- <property name="use_markup">True</property>
3951- </widget>
3952- <packing>
3953- <property name="type">label_item</property>
3954- </packing>
3955- </child>
3956- </widget>
3957- <packing>
3958- <property name="expand">False</property>
3959- <property name="position">1</property>
3960- </packing>
3961- </child>
3962- </widget>
3963- <packing>
3964- <property name="position">1</property>
3965- </packing>
3966- </child>
3967- <child>
3968- <widget class="GtkLabel" id="label10">
3969- <property name="visible">True</property>
3970- <property name="label" translatable="yes">Music</property>
3971- </widget>
3972- <packing>
3973- <property name="position">1</property>
3974- <property name="tab_fill">False</property>
3975- <property name="type">tab</property>
3976- </packing>
3977- </child>
3978- <child>
3979- <widget class="GtkVBox" id="vbox14">
3980- <property name="visible">True</property>
3981- <child>
3982- <widget class="GtkFrame" id="frame_image_folders3">
3983- <property name="visible">True</property>
3984- <property name="border_width">5</property>
3985- <property name="label_xalign">0</property>
3986- <property name="shadow_type">none</property>
3987- <child>
3988- <widget class="GtkAlignment" id="alignment11">
3989- <property name="visible">True</property>
3990- <property name="left_padding">12</property>
3991- <child>
3992- <widget class="GtkVBox" id="vbox15">
3993- <property name="visible">True</property>
3994- <child>
3995- <widget class="GtkLabel" id="label_music_tip3">
3996- <property name="visible">True</property>
3997- <property name="xalign">0</property>
3998- <property name="label" translatable="yes">Entertainer image library scans data from the listed folders.</property>
3999- </widget>
4000- <packing>
4001- <property name="expand">False</property>
4002- <property name="padding">5</property>
4003- <property name="position">0</property>
4004- </packing>
4005- </child>
4006- <child>
4007- <widget class="GtkHBox" id="hbox14">
4008- <property name="visible">True</property>
4009- <child>
4010- <widget class="GtkScrolledWindow" id="scrolledwindow6">
4011- <property name="visible">True</property>
4012- <property name="can_focus">True</property>
4013- <property name="hscrollbar_policy">automatic</property>
4014- <property name="vscrollbar_policy">automatic</property>
4015- <property name="shadow_type">in</property>
4016- <child>
4017- <widget class="GtkTreeView" id="treeview_images">
4018- <property name="visible">True</property>
4019- <property name="can_focus">True</property>
4020- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4021- <property name="enable_search">False</property>
4022- </widget>
4023- </child>
4024- </widget>
4025- <packing>
4026- <property name="position">0</property>
4027- </packing>
4028- </child>
4029- <child>
4030- <widget class="GtkVButtonBox" id="vbuttonbox7">
4031- <property name="visible">True</property>
4032- <property name="spacing">5</property>
4033- <property name="layout_style">start</property>
4034- <child>
4035- <widget class="GtkButton" id="button_add_images">
4036- <property name="label">gtk-add</property>
4037- <property name="visible">True</property>
4038- <property name="can_focus">True</property>
4039- <property name="receives_default">False</property>
4040- <property name="use_stock">True</property>
4041- <signal name="clicked" handler="on_button_add_images_clicked"/>
4042- </widget>
4043- <packing>
4044- <property name="expand">False</property>
4045- <property name="fill">False</property>
4046- <property name="position">0</property>
4047- </packing>
4048- </child>
4049- <child>
4050- <widget class="GtkButton" id="button_remove_images">
4051- <property name="label">gtk-remove</property>
4052- <property name="visible">True</property>
4053- <property name="can_focus">True</property>
4054- <property name="receives_default">False</property>
4055- <property name="use_stock">True</property>
4056- <signal name="clicked" handler="on_button_remove_images_clicked"/>
4057- </widget>
4058- <packing>
4059- <property name="expand">False</property>
4060- <property name="fill">False</property>
4061- <property name="position">1</property>
4062- </packing>
4063- </child>
4064- <child>
4065- <widget class="GtkButton" id="button_edit_images">
4066- <property name="label">gtk-edit</property>
4067- <property name="visible">True</property>
4068- <property name="can_focus">True</property>
4069- <property name="receives_default">False</property>
4070- <property name="use_stock">True</property>
4071- <signal name="clicked" handler="on_button_edit_images_clicked"/>
4072- </widget>
4073- <packing>
4074- <property name="expand">False</property>
4075- <property name="fill">False</property>
4076- <property name="position">2</property>
4077- </packing>
4078- </child>
4079- </widget>
4080- <packing>
4081- <property name="expand">False</property>
4082- <property name="padding">5</property>
4083- <property name="position">1</property>
4084- </packing>
4085- </child>
4086- </widget>
4087- <packing>
4088- <property name="position">1</property>
4089- </packing>
4090- </child>
4091- </widget>
4092- </child>
4093- </widget>
4094- </child>
4095- <child>
4096- <widget class="GtkLabel" id="label11">
4097- <property name="visible">True</property>
4098- <property name="label" translatable="yes">&lt;b&gt;Image library&lt;/b&gt;</property>
4099- <property name="use_markup">True</property>
4100- </widget>
4101- <packing>
4102- <property name="type">label_item</property>
4103- </packing>
4104- </child>
4105- </widget>
4106- <packing>
4107- <property name="position">0</property>
4108- </packing>
4109- </child>
4110- <child>
4111- <widget class="GtkFrame" id="frame_image_lib_settings3">
4112- <property name="visible">True</property>
4113- <property name="border_width">5</property>
4114- <property name="label_xalign">0</property>
4115- <property name="shadow_type">none</property>
4116- <child>
4117- <widget class="GtkAlignment" id="alignment12">
4118- <property name="visible">True</property>
4119- <property name="left_padding">12</property>
4120- <child>
4121- <widget class="GtkVBox" id="vbox16">
4122- <property name="visible">True</property>
4123- <child>
4124- <placeholder/>
4125- </child>
4126- <child>
4127- <widget class="GtkCheckButton" id="hidden_files_folders_checkbox">
4128- <property name="label" translatable="yes">Display hidden files and folders in image library</property>
4129- <property name="visible">True</property>
4130- <property name="can_focus">True</property>
4131- <property name="receives_default">False</property>
4132- <property name="use_underline">True</property>
4133- <property name="draw_indicator">True</property>
4134- <signal name="toggled" handler="on_hidden_files_folders_checkbox_toggled"/>
4135- </widget>
4136- <packing>
4137- <property name="padding">5</property>
4138- <property name="position">1</property>
4139- </packing>
4140- </child>
4141- </widget>
4142- </child>
4143- </widget>
4144- </child>
4145- <child>
4146- <widget class="GtkLabel" id="label_image_lib_settings3">
4147- <property name="visible">True</property>
4148- <property name="label" translatable="yes">&lt;b&gt;Image library settings&lt;/b&gt;</property>
4149- <property name="use_markup">True</property>
4150- </widget>
4151- <packing>
4152- <property name="type">label_item</property>
4153- </packing>
4154- </child>
4155- </widget>
4156- <packing>
4157- <property name="expand">False</property>
4158- <property name="position">1</property>
4159- </packing>
4160- </child>
4161- </widget>
4162- <packing>
4163- <property name="position">2</property>
4164- </packing>
4165- </child>
4166- <child>
4167- <widget class="GtkLabel" id="label_images3">
4168- <property name="visible">True</property>
4169- <property name="label" translatable="yes">Images</property>
4170- </widget>
4171- <packing>
4172- <property name="position">2</property>
4173- <property name="tab_fill">False</property>
4174- <property name="type">tab</property>
4175- </packing>
4176- </child>
4177- <child>
4178- <widget class="GtkVBox" id="vbox_feeds">
4179- <property name="visible">True</property>
4180- <child>
4181- <widget class="GtkFrame" id="frame_rss_feeds3">
4182- <property name="visible">True</property>
4183- <property name="border_width">5</property>
4184- <property name="label_xalign">0</property>
4185- <property name="shadow_type">none</property>
4186- <child>
4187- <widget class="GtkAlignment" id="alignment13">
4188- <property name="visible">True</property>
4189- <property name="left_padding">12</property>
4190- <child>
4191- <widget class="GtkVBox" id="vbox17">
4192- <property name="visible">True</property>
4193- <child>
4194- <widget class="GtkLabel" id="label_rss_tip3">
4195- <property name="visible">True</property>
4196- <property name="xalign">0</property>
4197- <property name="label" translatable="yes">Below is a list of RSS feeds that are displayd in Entertainer.</property>
4198- </widget>
4199- <packing>
4200- <property name="expand">False</property>
4201- <property name="padding">5</property>
4202- <property name="position">0</property>
4203- </packing>
4204- </child>
4205- <child>
4206- <widget class="GtkHBox" id="hbox16">
4207- <property name="visible">True</property>
4208- <child>
4209- <widget class="GtkScrolledWindow" id="scrolledwindow_rss3">
4210- <property name="visible">True</property>
4211- <property name="can_focus">True</property>
4212- <property name="hscrollbar_policy">automatic</property>
4213- <property name="vscrollbar_policy">automatic</property>
4214- <property name="shadow_type">in</property>
4215- <child>
4216- <widget class="GtkTreeView" id="treeview_feeds">
4217- <property name="visible">True</property>
4218- <property name="can_focus">True</property>
4219- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4220- <property name="enable_search">False</property>
4221- </widget>
4222- </child>
4223- </widget>
4224- <packing>
4225- <property name="position">0</property>
4226- </packing>
4227- </child>
4228- <child>
4229- <widget class="GtkVButtonBox" id="vbuttonbox8">
4230- <property name="visible">True</property>
4231- <property name="spacing">5</property>
4232- <property name="layout_style">start</property>
4233- <child>
4234- <widget class="GtkButton" id="button_add_feed">
4235- <property name="label">gtk-add</property>
4236- <property name="visible">True</property>
4237- <property name="can_focus">True</property>
4238- <property name="receives_default">False</property>
4239- <property name="tooltip" translatable="yes">Add RSS Feed</property>
4240- <property name="use_stock">True</property>
4241- <signal name="clicked" handler="on_button_add_feed_clicked"/>
4242- </widget>
4243- <packing>
4244- <property name="expand">False</property>
4245- <property name="fill">False</property>
4246- <property name="position">0</property>
4247- </packing>
4248- </child>
4249- <child>
4250- <widget class="GtkButton" id="button_remove_feed">
4251- <property name="label">gtk-remove</property>
4252- <property name="visible">True</property>
4253- <property name="can_focus">True</property>
4254- <property name="receives_default">False</property>
4255- <property name="tooltip" translatable="yes">Remove RSS Feed</property>
4256- <property name="use_stock">True</property>
4257- <signal name="clicked" handler="on_button_remove_feed_clicked"/>
4258- </widget>
4259- <packing>
4260- <property name="expand">False</property>
4261- <property name="fill">False</property>
4262- <property name="position">1</property>
4263- </packing>
4264- </child>
4265- <child>
4266- <widget class="GtkButton" id="button_edit_feed">
4267- <property name="label">gtk-edit</property>
4268- <property name="visible">True</property>
4269- <property name="can_focus">True</property>
4270- <property name="receives_default">False</property>
4271- <property name="tooltip" translatable="yes">Edit current item</property>
4272- <property name="use_stock">True</property>
4273- <signal name="clicked" handler="on_button_edit_feed_clicked"/>
4274- </widget>
4275- <packing>
4276- <property name="expand">False</property>
4277- <property name="fill">False</property>
4278- <property name="position">2</property>
4279- </packing>
4280- </child>
4281- <child>
4282- <widget class="GtkButton" id="button_open_list">
4283- <property name="label">gtk-open</property>
4284- <property name="visible">True</property>
4285- <property name="can_focus">True</property>
4286- <property name="receives_default">False</property>
4287- <property name="tooltip" translatable="yes">Add feeds from a datasource</property>
4288- <property name="use_stock">True</property>
4289- <signal name="clicked" handler="on_button_open_list_clicked"/>
4290- </widget>
4291- <packing>
4292- <property name="expand">False</property>
4293- <property name="fill">False</property>
4294- <property name="position">3</property>
4295- </packing>
4296- </child>
4297- </widget>
4298- <packing>
4299- <property name="expand">False</property>
4300- <property name="padding">5</property>
4301- <property name="position">1</property>
4302- </packing>
4303- </child>
4304- </widget>
4305- <packing>
4306- <property name="position">1</property>
4307- </packing>
4308- </child>
4309- </widget>
4310- </child>
4311- </widget>
4312- </child>
4313- <child>
4314- <widget class="GtkLabel" id="label_rss_sources3">
4315- <property name="visible">True</property>
4316- <property name="label" translatable="yes">&lt;b&gt;RSS feeds&lt;/b&gt;</property>
4317- <property name="use_markup">True</property>
4318- </widget>
4319- <packing>
4320- <property name="type">label_item</property>
4321- </packing>
4322- </child>
4323- </widget>
4324- <packing>
4325- <property name="position">0</property>
4326- </packing>
4327- </child>
4328- <child>
4329- <widget class="GtkFrame" id="frame_feed_settings3">
4330- <property name="visible">True</property>
4331- <property name="border_width">5</property>
4332- <property name="label_xalign">0</property>
4333- <property name="shadow_type">none</property>
4334- <child>
4335- <widget class="GtkAlignment" id="alignment14">
4336- <property name="visible">True</property>
4337- <property name="left_padding">12</property>
4338- <child>
4339- <widget class="GtkVBox" id="vbox18">
4340- <property name="visible">True</property>
4341- <child>
4342- <widget class="GtkHBox" id="hbox17">
4343- <property name="visible">True</property>
4344- <child>
4345- <widget class="GtkLabel" id="label12">
4346- <property name="visible">True</property>
4347- <property name="label" translatable="yes">Update all feeds every</property>
4348- </widget>
4349- <packing>
4350- <property name="expand">False</property>
4351- <property name="position">0</property>
4352- </packing>
4353- </child>
4354- <child>
4355- <widget class="GtkSpinButton" id="fetch_interval_spinbutton">
4356- <property name="visible">True</property>
4357- <property name="can_focus">True</property>
4358- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4359- <property name="adjustment">60 15 900 1 10 0</property>
4360- <property name="climb_rate">1</property>
4361- <property name="numeric">True</property>
4362- <signal name="value_changed" handler="on_fetch_interval_spinbutton_value_changed"/>
4363- </widget>
4364- <packing>
4365- <property name="expand">False</property>
4366- <property name="padding">5</property>
4367- <property name="position">1</property>
4368- </packing>
4369- </child>
4370- <child>
4371- <widget class="GtkLabel" id="label13">
4372- <property name="visible">True</property>
4373- <property name="label" translatable="yes">minutes.</property>
4374- </widget>
4375- <packing>
4376- <property name="expand">False</property>
4377- <property name="position">2</property>
4378- </packing>
4379- </child>
4380- </widget>
4381- <packing>
4382- <property name="expand">False</property>
4383- <property name="padding">10</property>
4384- <property name="position">0</property>
4385- </packing>
4386- </child>
4387- </widget>
4388- </child>
4389- </widget>
4390- </child>
4391- <child>
4392- <widget class="GtkLabel" id="label_rss_settings3">
4393- <property name="visible">True</property>
4394- <property name="label" translatable="yes">&lt;b&gt;Feed settings&lt;/b&gt;</property>
4395- <property name="use_markup">True</property>
4396- </widget>
4397- <packing>
4398- <property name="type">label_item</property>
4399- </packing>
4400- </child>
4401- </widget>
4402- <packing>
4403- <property name="expand">False</property>
4404- <property name="position">1</property>
4405- </packing>
4406- </child>
4407- </widget>
4408- <packing>
4409- <property name="position">3</property>
4410- </packing>
4411- </child>
4412- <child>
4413- <widget class="GtkLabel" id="label_rss3">
4414- <property name="visible">True</property>
4415- <property name="label" translatable="yes">RSS feeds</property>
4416- </widget>
4417- <packing>
4418- <property name="position">3</property>
4419- <property name="tab_fill">False</property>
4420- <property name="type">tab</property>
4421- </packing>
4422- </child>
4423- <child>
4424- <widget class="GtkVBox" id="vbox5">
4425- <property name="visible">True</property>
4426- <child>
4427- <widget class="GtkFrame" id="frame_video_lib3">
4428- <property name="visible">True</property>
4429- <property name="border_width">5</property>
4430- <property name="label_xalign">0</property>
4431- <property name="shadow_type">none</property>
4432- <child>
4433- <widget class="GtkAlignment" id="alignment5">
4434- <property name="visible">True</property>
4435- <property name="left_padding">12</property>
4436- <child>
4437- <widget class="GtkVBox" id="weather_location_list_area">
4438- <property name="visible">True</property>
4439- <child>
4440- <widget class="GtkLabel" id="label_weather_tip">
4441- <property name="visible">True</property>
4442- <property name="xalign">0</property>
4443- <property name="label" translatable="yes">Get weather conditions from the locations listed below.</property>
4444- <property name="single_line_mode">True</property>
4445- </widget>
4446- <packing>
4447- <property name="expand">False</property>
4448- <property name="padding">5</property>
4449- <property name="position">0</property>
4450- </packing>
4451- </child>
4452- <child>
4453- <widget class="GtkHBox" id="hbox4">
4454- <property name="visible">True</property>
4455- <child>
4456- <widget class="GtkScrolledWindow" id="scrolledwindow3">
4457- <property name="visible">True</property>
4458- <property name="can_focus">True</property>
4459- <property name="hscrollbar_policy">automatic</property>
4460- <property name="vscrollbar_policy">automatic</property>
4461- <property name="shadow_type">in</property>
4462- <child>
4463- <widget class="GtkTreeView" id="treeview_locations">
4464- <property name="visible">True</property>
4465- <property name="can_focus">True</property>
4466- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4467- <property name="enable_search">False</property>
4468- </widget>
4469- </child>
4470- </widget>
4471- <packing>
4472- <property name="position">0</property>
4473- </packing>
4474- </child>
4475- <child>
4476- <widget class="GtkVButtonBox" id="vbuttonbox3">
4477- <property name="visible">True</property>
4478- <property name="spacing">5</property>
4479- <property name="layout_style">start</property>
4480- <child>
4481- <widget class="GtkButton" id="button_add_weather">
4482- <property name="label">gtk-new</property>
4483- <property name="visible">True</property>
4484- <property name="can_focus">True</property>
4485- <property name="has_focus">True</property>
4486- <property name="can_default">True</property>
4487- <property name="has_default">True</property>
4488- <property name="receives_default">False</property>
4489- <property name="use_stock">True</property>
4490- <signal name="clicked" handler="on_button_add_weather_clicked"/>
4491- </widget>
4492- <packing>
4493- <property name="expand">False</property>
4494- <property name="fill">False</property>
4495- <property name="position">0</property>
4496- </packing>
4497- </child>
4498- <child>
4499- <widget class="GtkButton" id="button_remove_weather">
4500- <property name="label">gtk-clear</property>
4501- <property name="visible">True</property>
4502- <property name="can_focus">True</property>
4503- <property name="receives_default">False</property>
4504- <property name="use_stock">True</property>
4505- <signal name="clicked" handler="on_button_remove_weather_clicked"/>
4506- </widget>
4507- <packing>
4508- <property name="expand">False</property>
4509- <property name="fill">False</property>
4510- <property name="position">1</property>
4511- </packing>
4512- </child>
4513- </widget>
4514- <packing>
4515- <property name="expand">False</property>
4516- <property name="padding">5</property>
4517- <property name="position">1</property>
4518- </packing>
4519- </child>
4520- </widget>
4521- <packing>
4522- <property name="position">1</property>
4523- </packing>
4524- </child>
4525- </widget>
4526- </child>
4527- </widget>
4528- </child>
4529- <child>
4530- <widget class="GtkLabel" id="label_weather_options">
4531- <property name="visible">True</property>
4532- <property name="label" translatable="yes">&lt;b&gt;Weather locations&lt;/b&gt;</property>
4533- <property name="use_markup">True</property>
4534- </widget>
4535- <packing>
4536- <property name="type">label_item</property>
4537- </packing>
4538- </child>
4539- </widget>
4540- <packing>
4541- <property name="position">0</property>
4542- </packing>
4543- </child>
4544- <child>
4545- <widget class="GtkFrame" id="frame4">
4546- <property name="visible">True</property>
4547- <property name="label_xalign">0</property>
4548- <property name="shadow_type">none</property>
4549- <child>
4550- <widget class="GtkAlignment" id="alignment7">
4551- <property name="visible">True</property>
4552- <property name="left_padding">12</property>
4553- <child>
4554- <widget class="GtkVBox" id="vbox7">
4555- <property name="visible">True</property>
4556- <child>
4557- <widget class="GtkCheckButton" id="weather_display_checkbox">
4558- <property name="label" translatable="yes">Display weather option in main menu</property>
4559- <property name="visible">True</property>
4560- <property name="can_focus">True</property>
4561- <property name="receives_default">False</property>
4562- <property name="use_underline">True</property>
4563- <property name="active">True</property>
4564- <property name="draw_indicator">True</property>
4565- <signal name="toggled" handler="on_weather_display_checkbox_toggled"/>
4566- </widget>
4567- <packing>
4568- <property name="fill">False</property>
4569- <property name="position">0</property>
4570- </packing>
4571- </child>
4572- </widget>
4573- </child>
4574- </widget>
4575- </child>
4576- <child>
4577- <widget class="GtkLabel" id="label7">
4578- <property name="visible">True</property>
4579- <property name="label" translatable="yes">&lt;b&gt;Weather settings&lt;/b&gt;</property>
4580- <property name="use_markup">True</property>
4581- </widget>
4582- <packing>
4583- <property name="type">label_item</property>
4584- </packing>
4585- </child>
4586- </widget>
4587- <packing>
4588- <property name="position">1</property>
4589- </packing>
4590- </child>
4591- </widget>
4592- <packing>
4593- <property name="position">4</property>
4594- </packing>
4595- </child>
4596- <child>
4597- <widget class="GtkLabel" id="label_weather">
4598- <property name="visible">True</property>
4599- <property name="yalign">0.40999999642372131</property>
4600- <property name="label" translatable="yes">Weather</property>
4601- </widget>
4602- <packing>
4603- <property name="position">4</property>
4604- <property name="tab_fill">False</property>
4605- <property name="type">tab</property>
4606- </packing>
4607- </child>
4608- <child>
4609- <widget class="GtkFrame" id="frame2">
4610- <property name="visible">True</property>
4611- <property name="border_width">5</property>
4612- <property name="label_xalign">0</property>
4613- <property name="shadow_type">none</property>
4614- <child>
4615- <widget class="GtkAlignment" id="alignment3">
4616- <property name="visible">True</property>
4617- <property name="left_padding">12</property>
4618- <child>
4619- <widget class="GtkVBox" id="vbox3">
4620- <property name="visible">True</property>
4621- <child>
4622- <widget class="GtkLabel" id="label5">
4623- <property name="visible">True</property>
4624- <property name="xpad">7</property>
4625- <property name="ypad">7</property>
4626- <property name="label" translatable="yes">Rebuilding cache means that all data is removed and then
4627-folders are indexed again. This fixes broken media cache.</property>
4628- </widget>
4629- <packing>
4630- <property name="position">0</property>
4631- </packing>
4632- </child>
4633- <child>
4634- <widget class="GtkButton" id="button_video_rebuild">
4635- <property name="label" translatable="yes">Rebuild video cache</property>
4636- <property name="visible">True</property>
4637- <property name="can_focus">True</property>
4638- <property name="receives_default">False</property>
4639- <property name="border_width">5</property>
4640- <property name="use_underline">True</property>
4641- <signal name="clicked" handler="on_button_video_rebuild_clicked"/>
4642- </widget>
4643- <packing>
4644- <property name="position">1</property>
4645- </packing>
4646- </child>
4647- <child>
4648- <widget class="GtkButton" id="button_music_rebuild">
4649- <property name="label" translatable="yes">Rebuild music cache</property>
4650- <property name="visible">True</property>
4651- <property name="can_focus">True</property>
4652- <property name="receives_default">False</property>
4653- <property name="border_width">5</property>
4654- <property name="use_underline">True</property>
4655- <signal name="clicked" handler="on_button_music_rebuild_clicked"/>
4656- </widget>
4657- <packing>
4658- <property name="position">2</property>
4659- </packing>
4660- </child>
4661- <child>
4662- <widget class="GtkButton" id="button_image_rebuild">
4663- <property name="label" translatable="yes">Rebuild image cache</property>
4664- <property name="visible">True</property>
4665- <property name="can_focus">True</property>
4666- <property name="receives_default">False</property>
4667- <property name="border_width">5</property>
4668- <property name="use_underline">True</property>
4669- <signal name="clicked" handler="on_button_image_rebuild_clicked"/>
4670- </widget>
4671- <packing>
4672- <property name="position">3</property>
4673- </packing>
4674- </child>
4675- <child>
4676- <widget class="GtkButton" id="button_feed_rebuild">
4677- <property name="label" translatable="yes">Rebuild RSS-feed cache</property>
4678- <property name="visible">True</property>
4679- <property name="can_focus">True</property>
4680- <property name="receives_default">False</property>
4681- <property name="border_width">5</property>
4682- <property name="use_underline">True</property>
4683- <property name="focus_on_click">False</property>
4684- <signal name="clicked" handler="on_button_feed_rebuild_clicked"/>
4685- </widget>
4686- <packing>
4687- <property name="position">4</property>
4688- </packing>
4689- </child>
4690- </widget>
4691- </child>
4692- </widget>
4693- </child>
4694- <child>
4695- <widget class="GtkLabel" id="label4">
4696- <property name="visible">True</property>
4697- <property name="label" translatable="yes">&lt;b&gt;Media cache management&lt;/b&gt;</property>
4698- <property name="use_markup">True</property>
4699- </widget>
4700- <packing>
4701- <property name="type">label_item</property>
4702- </packing>
4703- </child>
4704- </widget>
4705- <packing>
4706- <property name="position">5</property>
4707- </packing>
4708- </child>
4709- <child>
4710- <widget class="GtkLabel" id="label3">
4711- <property name="visible">True</property>
4712- <property name="label" translatable="yes">Reset</property>
4713- </widget>
4714- <packing>
4715- <property name="position">5</property>
4716- <property name="tab_fill">False</property>
4717- <property name="type">tab</property>
4718- </packing>
4719- </child>
4720- </widget>
4721- <packing>
4722- <property name="position">1</property>
4723- </packing>
4724- </child>
4725- <child internal-child="action_area">
4726- <widget class="GtkHButtonBox" id="dialog-closebutton-area">
4727- <property name="visible">True</property>
4728- <property name="layout_style">end</property>
4729- <child>
4730- <widget class="GtkButton" id="close_button">
4731- <property name="label">gtk-close</property>
4732- <property name="visible">True</property>
4733- <property name="can_focus">True</property>
4734- <property name="receives_default">False</property>
4735- <property name="use_stock">True</property>
4736- <signal name="clicked" handler="on_close_button_clicked"/>
4737- </widget>
4738- <packing>
4739- <property name="expand">False</property>
4740- <property name="fill">False</property>
4741- <property name="position">0</property>
4742- </packing>
4743- </child>
4744- </widget>
4745- <packing>
4746- <property name="expand">False</property>
4747- <property name="pack_type">end</property>
4748- <property name="position">0</property>
4749- </packing>
4750- </child>
4751- </widget>
4752- </child>
4753- </widget>
4754- <widget class="GtkDialog" id="url_dialog">
4755- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4756- <property name="border_width">5</property>
4757- <property name="resizable">False</property>
4758- <property name="modal">True</property>
4759- <property name="window_position">center-on-parent</property>
4760- <property name="destroy_with_parent">True</property>
4761- <property name="icon_name">gtk-edit</property>
4762- <property name="type_hint">normal</property>
4763- <property name="skip_taskbar_hint">True</property>
4764- <property name="has_separator">False</property>
4765- <signal name="delete_event" handler="on_url_dialog_delete_event"/>
4766- <child internal-child="vbox">
4767- <widget class="GtkVBox" id="dialog-vbox2">
4768- <property name="visible">True</property>
4769- <property name="spacing">2</property>
4770- <child>
4771- <widget class="GtkHBox" id="hbox1">
4772- <property name="visible">True</property>
4773- <child>
4774- <widget class="GtkLabel" id="label1">
4775- <property name="visible">True</property>
4776- <property name="label" translatable="yes">URL:</property>
4777- </widget>
4778- <packing>
4779- <property name="expand">False</property>
4780- <property name="padding">5</property>
4781- <property name="position">0</property>
4782- </packing>
4783- </child>
4784- <child>
4785- <widget class="GtkEntry" id="url_entry">
4786- <property name="visible">True</property>
4787- <property name="can_focus">True</property>
4788- <property name="has_focus">True</property>
4789- <property name="can_default">True</property>
4790- <property name="has_default">True</property>
4791- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4792- </widget>
4793- <packing>
4794- <property name="position">1</property>
4795- </packing>
4796- </child>
4797- </widget>
4798- <packing>
4799- <property name="position">1</property>
4800- </packing>
4801- </child>
4802- <child internal-child="action_area">
4803- <widget class="GtkHButtonBox" id="dialog-action_area2">
4804- <property name="visible">True</property>
4805- <property name="layout_style">end</property>
4806- <child>
4807- <widget class="GtkButton" id="url_dialog_cancel_button">
4808- <property name="label">gtk-cancel</property>
4809- <property name="visible">True</property>
4810- <property name="can_focus">True</property>
4811- <property name="receives_default">False</property>
4812- <property name="use_stock">True</property>
4813- <signal name="clicked" handler="on_url_dialog_cancel_button_clicked"/>
4814- </widget>
4815- <packing>
4816- <property name="expand">False</property>
4817- <property name="fill">False</property>
4818- <property name="position">0</property>
4819- </packing>
4820- </child>
4821- <child>
4822- <widget class="GtkButton" id="url_dialog_ok_button">
4823- <property name="label">gtk-ok</property>
4824- <property name="visible">True</property>
4825- <property name="can_focus">True</property>
4826- <property name="receives_default">False</property>
4827- <property name="use_stock">True</property>
4828- <signal name="clicked" handler="on_url_dialog_ok_button_clicked"/>
4829- </widget>
4830- <packing>
4831- <property name="expand">False</property>
4832- <property name="fill">False</property>
4833- <property name="position">1</property>
4834- </packing>
4835- </child>
4836- </widget>
4837- <packing>
4838- <property name="expand">False</property>
4839- <property name="pack_type">end</property>
4840- <property name="position">0</property>
4841- </packing>
4842- </child>
4843- </widget>
4844- </child>
4845- </widget>
4846- <widget class="GtkDialog" id="weather_search_dialog">
4847- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4848- <property name="border_width">5</property>
4849- <property name="window_position">center-on-parent</property>
4850- <property name="type_hint">dialog</property>
4851- <property name="has_separator">False</property>
4852- <child internal-child="vbox">
4853- <widget class="GtkVBox" id="vbox20">
4854- <property name="visible">True</property>
4855- <property name="spacing">2</property>
4856- <child>
4857- <widget class="GtkVBox" id="vbox21">
4858- <property name="visible">True</property>
4859- <child>
4860- <widget class="GtkFrame" id="frame6">
4861- <property name="visible">True</property>
4862- <property name="label_xalign">0</property>
4863- <property name="shadow_type">none</property>
4864- <child>
4865- <widget class="GtkAlignment" id="alignment16">
4866- <property name="visible">True</property>
4867- <property name="top_padding">5</property>
4868- <property name="left_padding">12</property>
4869- <child>
4870- <widget class="GtkHBox" id="hbox18">
4871- <property name="visible">True</property>
4872- <child>
4873- <widget class="GtkLabel" id="label19">
4874- <property name="visible">True</property>
4875- <property name="label" translatable="yes">City:</property>
4876- </widget>
4877- <packing>
4878- <property name="expand">False</property>
4879- <property name="position">0</property>
4880- </packing>
4881- </child>
4882- <child>
4883- <widget class="GtkEntry" id="location_entry">
4884- <property name="visible">True</property>
4885- <property name="can_focus">True</property>
4886- <property name="has_focus">True</property>
4887- <property name="can_default">True</property>
4888- <property name="has_default">True</property>
4889- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4890- <signal name="activate" handler="on_location_entry_activate"/>
4891- </widget>
4892- <packing>
4893- <property name="padding">5</property>
4894- <property name="position">1</property>
4895- </packing>
4896- </child>
4897- <child>
4898- <widget class="GtkButton" id="location_find_button">
4899- <property name="label">gtk-find</property>
4900- <property name="visible">True</property>
4901- <property name="can_focus">True</property>
4902- <property name="receives_default">False</property>
4903- <property name="use_stock">True</property>
4904- <signal name="clicked" handler="on_location_find_button_clicked"/>
4905- </widget>
4906- <packing>
4907- <property name="padding">5</property>
4908- <property name="position">2</property>
4909- </packing>
4910- </child>
4911- </widget>
4912- </child>
4913- </widget>
4914- </child>
4915- <child>
4916- <widget class="GtkLabel" id="label20">
4917- <property name="visible">True</property>
4918- <property name="label" translatable="yes">&lt;b&gt;Search for location&lt;/b&gt;</property>
4919- <property name="use_markup">True</property>
4920- </widget>
4921- <packing>
4922- <property name="type">label_item</property>
4923- </packing>
4924- </child>
4925- </widget>
4926- <packing>
4927- <property name="expand">False</property>
4928- <property name="padding">5</property>
4929- <property name="position">0</property>
4930- </packing>
4931- </child>
4932- <child>
4933- <widget class="GtkFrame" id="frame7">
4934- <property name="visible">True</property>
4935- <property name="label_xalign">0</property>
4936- <property name="shadow_type">none</property>
4937- <child>
4938- <widget class="GtkAlignment" id="alignment17">
4939- <property name="visible">True</property>
4940- <property name="left_padding">12</property>
4941- <child>
4942- <widget class="GtkScrolledWindow" id="scrolledwindow7">
4943- <property name="visible">True</property>
4944- <property name="can_focus">True</property>
4945- <property name="border_width">5</property>
4946- <property name="hscrollbar_policy">never</property>
4947- <property name="vscrollbar_policy">automatic</property>
4948- <property name="shadow_type">etched-in</property>
4949- <child>
4950- <widget class="GtkTreeView" id="location_results_treeview">
4951- <property name="visible">True</property>
4952- <property name="can_focus">True</property>
4953- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
4954- </widget>
4955- </child>
4956- </widget>
4957- </child>
4958- </widget>
4959- </child>
4960- <child>
4961- <widget class="GtkLabel" id="label21">
4962- <property name="visible">True</property>
4963- <property name="label" translatable="yes">&lt;b&gt;Search result&lt;/b&gt;</property>
4964- <property name="use_markup">True</property>
4965- </widget>
4966- <packing>
4967- <property name="type">label_item</property>
4968- </packing>
4969- </child>
4970- </widget>
4971- <packing>
4972- <property name="padding">5</property>
4973- <property name="position">1</property>
4974- </packing>
4975- </child>
4976- </widget>
4977- <packing>
4978- <property name="position">1</property>
4979- </packing>
4980- </child>
4981- <child internal-child="action_area">
4982- <widget class="GtkHButtonBox" id="hbuttonbox1">
4983- <property name="visible">True</property>
4984- <property name="layout_style">end</property>
4985- <child>
4986- <widget class="GtkButton" id="location_button_cancel">
4987- <property name="label">gtk-cancel</property>
4988- <property name="visible">True</property>
4989- <property name="can_focus">True</property>
4990- <property name="receives_default">False</property>
4991- <property name="use_stock">True</property>
4992- <signal name="clicked" handler="on_location_cancel_button_clicked"/>
4993- </widget>
4994- <packing>
4995- <property name="expand">False</property>
4996- <property name="fill">False</property>
4997- <property name="position">0</property>
4998- </packing>
4999- </child>
5000- <child>
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches