1# Modified to work with Exaile - Brian Parma
2#
3# Copyright (C) 2008 Erik Hetzner
4
5# This file is part of Spydaap. Spydaap is free software: you can
6# redistribute it and/or modify it under the terms of the GNU General
7# Public License as published by the Free Software Foundation, either
8# version 3 of the License, or (at your option) any later version.
9
10# Spydaap is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14
15# You should have received a copy of the GNU General Public License
16# along with Spydaap. If not, see <https://www.gnu.org/licenses/>.
17
18import http.server
19import socketserver
20import logging
21import select
22import socket
23import os
24
25import spydaap
26import spydaap.daap
27import spydaap.metadata
28import spydaap.containers
29import spydaap.cache
30import spydaap.server
31import spydaap.zeroconfimpl
32
33from xl import common, event, xdg
34
35# Notes for debugging:
36# You might want to run
37#    handle SIGPIPE nostop
38# when debugging this code in gdb.
39
40"""
41Notes for hunting down errors:
42If you run this plugin and a client stops playback on any file, expect this
43traceback:
44
45    Exception happened during processing of request from ('192.168.122.1', 34394)
46    Traceback (most recent call last):
47      File "/usr/lib64/python2.7/SocketServer.py", line 596, in process_request_thread
48        self.finish_request(request, client_address)
49      File "/usr/lib64/python2.7/SocketServer.py", line 331, in finish_request
50        self.RequestHandlerClass(request, client_address, self)
51      File "/usr/lib64/python2.7/SocketServer.py", line 654, in __init__
52        self.finish()
53      File "/usr/lib64/python2.7/SocketServer.py", line 713, in finish
54        self.wfile.close()
55      File "/usr/lib64/python2.7/socket.py", line 283, in close
56        self.flush()
57      File "/usr/lib64/python2.7/socket.py", line 307, in flush
58        self._sock.sendall(view[write_offset:write_offset+buffer_size])
59    error: [Errno 32] broken pipe
60
61This traceback is a result of getting a SIGPIPE, which is expected. The fact
62that it is not being handled in the python standard library is a bug which has
63been reported to https://bugs.python.org/issue14574
64See also: https://stackoverflow.com/questions/6063416/
65"""
66
67
68logger = logging.getLogger('daapserver')
69
70__all__ = ['DaapServer']
71
72
73class MyThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
74    """Handle requests in a separate thread."""
75
76    timeout = 1
77    daemon_threads = True
78
79    def __init__(self, *args):
80        if ':' in args[0][0]:
81            self.address_family = socket.AF_INET6
82        http.server.HTTPServer.__init__(self, *args)
83
84
85class DaapServer:
86    def __init__(self, library, name=spydaap.server_name, host='', port=spydaap.port):
87        #        Thread.__init__(self)
88        self.host = host
89        self.port = port
90        self.library = library
91        self.name = name
92        self.httpd = None
93        self.handler = None
94        self.__cache = spydaap.cache.Cache(os.path.join(xdg.cache_home, 'daapserver'))
95        self.__cache.clean()
96
97        # Set a callback that will let us propagate library changes to clients
98        event.add_callback(self.update_rev, 'libraries_modified', library.collection)
99
100    def update_rev(self, *args):
101        if self.handler is not None:
102            # Updating the server revision, so if a client checks
103            # it can see the library has changed
104            self.handler.daap_server_revision += 1
105            logger.info(
106                'Libraries Changed, incrementing revision to %d.'
107                % self.handler.daap_server_revision
108            )
109        self.__cache.clean()
110
111    def set(self, **kwargs):
112        for key in kwargs:
113            setattr(self, key, kwargs[key])
114
115    @common.threaded
116    def run(self):
117        self.zeroconf = spydaap.zeroconfimpl.ZeroconfImpl(
118            self.name, self.port, stype="_daap._tcp"
119        )
120        self.handler = spydaap.server.makeDAAPHandlerClass(
121            str(self.name), self.__cache, self.library, []
122        )
123        self.httpd = MyThreadedHTTPServer((self.host, self.port), self.handler)
124
125        # signal.signal(signal.SIGTERM, make_shutdown(httpd))
126        # signal.signal(signal.SIGHUP, rebuild_cache)
127        if self.httpd.address_family == socket.AF_INET:
128            self.zeroconf.publish(ipv4=True, ipv6=False)
129        else:
130            self.zeroconf.publish(ipv4=False, ipv6=True)
131
132        try:
133            try:
134                logger.info("DAAP server: Listening.")
135                self.httpd.serve_forever()
136            except select.error:
137                pass
138        except KeyboardInterrupt:
139            self.httpd.shutdown()
140
141        logger.info("DAAP server: Shutting down.")
142        self.zeroconf.unpublish()
143        self.httpd = None
144
145    def start(self):
146        if self.httpd is None:
147            self.run()
148            return True
149        return False
150
151    def stop(self):
152        if self.httpd is not None:
153            self.httpd.shutdown()
154            self.httpd.socket.close()
155            return True
156        return False
157
158    def stop_server(self):
159        self.stop()
160
161
162# def rebuild_cache(signum=None, frame=None):
163#    md_cache.build(os.path.abspath(spydaap.media_path))
164#    container_cache.clean()
165#    container_cache.build(md_cache)
166#    cache.clean()
167
168# def really_main():
169#    rebuild_cache()
170#    zeroconf = spydaap.zeroconfimpl.ZeroconfImpl(spydaap.server_name,
171#                                         spydaap.port,
172#                                         stype="_daap._tcp")
173#    zeroconf.publish()
174#    logger.warning("Listening.")
175#    httpd = MyThreadedHTTPServer(('0.0.0.0', spydaap.port),
176#                                 spydaap.server.makeDAAPHandlerClass(spydaap.server_name, cache, md_cache, container_cache))
177#
178##    signal.signal(signal.SIGTERM, make_shutdown(httpd))
179##    signal.signal(signal.SIGHUP, rebuild_cache)
180#
181#    try:
182#        try:
183#            httpd.serve_forever()
184#        except select.error:
185#            pass
186#    except KeyboardInterrupt:
187#        httpd.force_stop()
188#    logger.warning("Shutting down.")
189#    zeroconf.unpublish()
190
191# def main():
192#    really_main()
193
194# if __name__ == "__main__":
195#    main()
196