mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-20 16:03:59 +01:00
Big Update: Automatic updating, Show what albums/songs you already have, Config fixes, Fixed restart & shutdown buttons
This commit is contained in:
+16
-1
@@ -1,2 +1,17 @@
|
|||||||
|
# Compiled source #
|
||||||
|
###################
|
||||||
*.pyc
|
*.pyc
|
||||||
*.log
|
|
||||||
|
# Logs and databases #
|
||||||
|
######################
|
||||||
|
*.log
|
||||||
|
*.db
|
||||||
|
*.ini
|
||||||
|
logs/*
|
||||||
|
|
||||||
|
# OS generated files #
|
||||||
|
######################
|
||||||
|
.DS_Store?
|
||||||
|
ehthumbs.db
|
||||||
|
Icon?
|
||||||
|
Thumbs.db
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version_info = (2, 0, 0, 'rc', 2)
|
|
||||||
version = '.'.join(str(n) for n in version_info[:3])
|
|
||||||
release = version + ''.join(str(n) for n in version_info[3:])
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
__all__ = ('EVENT_SCHEDULER_START', 'EVENT_SCHEDULER_SHUTDOWN',
|
|
||||||
'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED',
|
|
||||||
'EVENT_JOBSTORE_JOB_ADDED', 'EVENT_JOBSTORE_JOB_REMOVED',
|
|
||||||
'EVENT_JOB_EXECUTED', 'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED',
|
|
||||||
'EVENT_ALL', 'SchedulerEvent', 'JobStoreEvent', 'JobEvent')
|
|
||||||
|
|
||||||
|
|
||||||
EVENT_SCHEDULER_START = 1 # The scheduler was started
|
|
||||||
EVENT_SCHEDULER_SHUTDOWN = 2 # The scheduler was shut down
|
|
||||||
EVENT_JOBSTORE_ADDED = 4 # A job store was added to the scheduler
|
|
||||||
EVENT_JOBSTORE_REMOVED = 8 # A job store was removed from the scheduler
|
|
||||||
EVENT_JOBSTORE_JOB_ADDED = 16 # A job was added to a job store
|
|
||||||
EVENT_JOBSTORE_JOB_REMOVED = 32 # A job was removed from a job store
|
|
||||||
EVENT_JOB_EXECUTED = 64 # A job was executed successfully
|
|
||||||
EVENT_JOB_ERROR = 128 # A job raised an exception during execution
|
|
||||||
EVENT_JOB_MISSED = 256 # A job's execution was missed
|
|
||||||
EVENT_ALL = (EVENT_SCHEDULER_START | EVENT_SCHEDULER_SHUTDOWN |
|
|
||||||
EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED |
|
|
||||||
EVENT_JOBSTORE_JOB_ADDED | EVENT_JOBSTORE_JOB_REMOVED |
|
|
||||||
EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED)
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerEvent(object):
|
|
||||||
"""
|
|
||||||
An event that concerns the scheduler itself.
|
|
||||||
|
|
||||||
:var code: the type code of this event
|
|
||||||
"""
|
|
||||||
def __init__(self, code):
|
|
||||||
self.code = code
|
|
||||||
|
|
||||||
|
|
||||||
class JobStoreEvent(SchedulerEvent):
|
|
||||||
"""
|
|
||||||
An event that concerns job stores.
|
|
||||||
|
|
||||||
:var alias: the alias of the job store involved
|
|
||||||
:var job: the new job if a job was added
|
|
||||||
"""
|
|
||||||
def __init__(self, code, alias, job=None):
|
|
||||||
SchedulerEvent.__init__(self, code)
|
|
||||||
self.alias = alias
|
|
||||||
if job:
|
|
||||||
self.job = job
|
|
||||||
|
|
||||||
|
|
||||||
class JobEvent(SchedulerEvent):
|
|
||||||
"""
|
|
||||||
An event that concerns the execution of individual jobs.
|
|
||||||
|
|
||||||
:var job: the job instance in question
|
|
||||||
:var scheduled_run_time: the time when the job was scheduled to be run
|
|
||||||
:var retval: the return value of the successfully executed job
|
|
||||||
:var exception: the exception raised by the job
|
|
||||||
:var traceback: the traceback object associated with the exception
|
|
||||||
"""
|
|
||||||
def __init__(self, code, job, scheduled_run_time, retval=None,
|
|
||||||
exception=None, traceback=None):
|
|
||||||
SchedulerEvent.__init__(self, code)
|
|
||||||
self.job = job
|
|
||||||
self.scheduled_run_time = scheduled_run_time
|
|
||||||
self.retval = retval
|
|
||||||
self.exception = exception
|
|
||||||
self.traceback = traceback
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"""
|
|
||||||
Jobs represent scheduled tasks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from threading import Lock
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from apscheduler.util import to_unicode, ref_to_obj, get_callable_name,\
|
|
||||||
obj_to_ref
|
|
||||||
|
|
||||||
|
|
||||||
class MaxInstancesReachedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Job(object):
|
|
||||||
"""
|
|
||||||
Encapsulates the actual Job along with its metadata. Job instances
|
|
||||||
are created by the scheduler when adding jobs, and it should not be
|
|
||||||
directly instantiated.
|
|
||||||
|
|
||||||
:param trigger: trigger that determines the execution times
|
|
||||||
:param func: callable to call when the trigger is triggered
|
|
||||||
:param args: list of positional arguments to call func with
|
|
||||||
:param kwargs: dict of keyword arguments to call func with
|
|
||||||
:param name: name of the job (optional)
|
|
||||||
:param misfire_grace_time: seconds after the designated run time that
|
|
||||||
the job is still allowed to be run
|
|
||||||
:param coalesce: run once instead of many times if the scheduler determines
|
|
||||||
that the job should be run more than once in succession
|
|
||||||
:param max_runs: maximum number of times this job is allowed to be
|
|
||||||
triggered
|
|
||||||
:param max_instances: maximum number of concurrently running
|
|
||||||
instances allowed for this job
|
|
||||||
"""
|
|
||||||
id = None
|
|
||||||
next_run_time = None
|
|
||||||
|
|
||||||
def __init__(self, trigger, func, args, kwargs, misfire_grace_time,
|
|
||||||
coalesce, name=None, max_runs=None, max_instances=1):
|
|
||||||
if not trigger:
|
|
||||||
raise ValueError('The trigger must not be None')
|
|
||||||
if not hasattr(func, '__call__'):
|
|
||||||
raise TypeError('func must be callable')
|
|
||||||
if not hasattr(args, '__getitem__'):
|
|
||||||
raise TypeError('args must be a list-like object')
|
|
||||||
if not hasattr(kwargs, '__getitem__'):
|
|
||||||
raise TypeError('kwargs must be a dict-like object')
|
|
||||||
if misfire_grace_time <= 0:
|
|
||||||
raise ValueError('misfire_grace_time must be a positive value')
|
|
||||||
if max_runs is not None and max_runs <= 0:
|
|
||||||
raise ValueError('max_runs must be a positive value')
|
|
||||||
if max_instances <= 0:
|
|
||||||
raise ValueError('max_instances must be a positive value')
|
|
||||||
|
|
||||||
self._lock = Lock()
|
|
||||||
|
|
||||||
self.trigger = trigger
|
|
||||||
self.func = func
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
self.name = to_unicode(name or get_callable_name(func))
|
|
||||||
self.misfire_grace_time = misfire_grace_time
|
|
||||||
self.coalesce = coalesce
|
|
||||||
self.max_runs = max_runs
|
|
||||||
self.max_instances = max_instances
|
|
||||||
self.runs = 0
|
|
||||||
self.instances = 0
|
|
||||||
|
|
||||||
def compute_next_run_time(self, now):
|
|
||||||
if self.runs == self.max_runs:
|
|
||||||
self.next_run_time = None
|
|
||||||
else:
|
|
||||||
self.next_run_time = self.trigger.get_next_fire_time(now)
|
|
||||||
|
|
||||||
return self.next_run_time
|
|
||||||
|
|
||||||
def get_run_times(self, now):
|
|
||||||
"""
|
|
||||||
Computes the scheduled run times between ``next_run_time`` and ``now``.
|
|
||||||
"""
|
|
||||||
run_times = []
|
|
||||||
run_time = self.next_run_time
|
|
||||||
increment = timedelta(microseconds=1)
|
|
||||||
while ((not self.max_runs or self.runs < self.max_runs) and
|
|
||||||
run_time and run_time <= now):
|
|
||||||
run_times.append(run_time)
|
|
||||||
run_time = self.trigger.get_next_fire_time(run_time + increment)
|
|
||||||
|
|
||||||
return run_times
|
|
||||||
|
|
||||||
def add_instance(self):
|
|
||||||
self._lock.acquire()
|
|
||||||
try:
|
|
||||||
if self.instances == self.max_instances:
|
|
||||||
raise MaxInstancesReachedError
|
|
||||||
self.instances += 1
|
|
||||||
finally:
|
|
||||||
self._lock.release()
|
|
||||||
|
|
||||||
def remove_instance(self):
|
|
||||||
self._lock.acquire()
|
|
||||||
try:
|
|
||||||
assert self.instances > 0, 'Already at 0 instances'
|
|
||||||
self.instances -= 1
|
|
||||||
finally:
|
|
||||||
self._lock.release()
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
# Prevents the unwanted pickling of transient or unpicklable variables
|
|
||||||
state = self.__dict__.copy()
|
|
||||||
state.pop('instances', None)
|
|
||||||
state.pop('func', None)
|
|
||||||
state.pop('_lock', None)
|
|
||||||
state['func_ref'] = obj_to_ref(self.func)
|
|
||||||
return state
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
state['instances'] = 0
|
|
||||||
state['func'] = ref_to_obj(state.pop('func_ref'))
|
|
||||||
state['_lock'] = Lock()
|
|
||||||
self.__dict__ = state
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, Job):
|
|
||||||
return self.id is not None and other.id == self.id or self is other
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Job (name=%s, trigger=%s)>' % (self.name, repr(self.trigger))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '%s (trigger: %s, next run at: %s)' % (self.name,
|
|
||||||
str(self.trigger), str(self.next_run_time))
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
Abstract base class that provides the interface needed by all job stores.
|
|
||||||
Job store methods are also documented here.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class JobStore(object):
|
|
||||||
def add_job(self, job):
|
|
||||||
"""Adds the given job from this store."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
"""Persists the running state of the given job."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def remove_job(self, job):
|
|
||||||
"""Removes the given jobs from this store."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def load_jobs(self):
|
|
||||||
"""Loads jobs from this store into memory."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Frees any resources still bound to this job store."""
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
"""
|
|
||||||
Stores jobs in a MongoDB database.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import JobStore
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from bson.binary import Binary
|
|
||||||
from pymongo.connection import Connection
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('MongoDBJobStore requires PyMongo installed')
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MongoDBJobStore(JobStore):
|
|
||||||
def __init__(self, database='apscheduler', collection='jobs',
|
|
||||||
connection=None, pickle_protocol=pickle.HIGHEST_PROTOCOL,
|
|
||||||
**connect_args):
|
|
||||||
self.jobs = []
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
|
|
||||||
if not database:
|
|
||||||
raise ValueError('The "database" parameter must not be empty')
|
|
||||||
if not collection:
|
|
||||||
raise ValueError('The "collection" parameter must not be empty')
|
|
||||||
|
|
||||||
if connection:
|
|
||||||
self.connection = connection
|
|
||||||
else:
|
|
||||||
self.connection = Connection(**connect_args)
|
|
||||||
|
|
||||||
self.collection = self.connection[database][collection]
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
job_dict = job.__getstate__()
|
|
||||||
job_dict['trigger'] = Binary(pickle.dumps(job.trigger,
|
|
||||||
self.pickle_protocol))
|
|
||||||
job_dict['args'] = Binary(pickle.dumps(job.args,
|
|
||||||
self.pickle_protocol))
|
|
||||||
job_dict['kwargs'] = Binary(pickle.dumps(job.kwargs,
|
|
||||||
self.pickle_protocol))
|
|
||||||
job.id = self.collection.insert(job_dict)
|
|
||||||
self.jobs.append(job)
|
|
||||||
|
|
||||||
def remove_job(self, job):
|
|
||||||
self.collection.remove(job.id)
|
|
||||||
self.jobs.remove(job)
|
|
||||||
|
|
||||||
def load_jobs(self):
|
|
||||||
jobs = []
|
|
||||||
for job_dict in self.collection.find():
|
|
||||||
try:
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job_dict['id'] = job_dict.pop('_id')
|
|
||||||
job_dict['trigger'] = pickle.loads(job_dict['trigger'])
|
|
||||||
job_dict['args'] = pickle.loads(job_dict['args'])
|
|
||||||
job_dict['kwargs'] = pickle.loads(job_dict['kwargs'])
|
|
||||||
job.__setstate__(job_dict)
|
|
||||||
jobs.append(job)
|
|
||||||
except Exception:
|
|
||||||
job_name = job_dict.get('name', '(unknown)')
|
|
||||||
logger.exception('Unable to restore job "%s"', job_name)
|
|
||||||
self.jobs = jobs
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
spec = {'_id': job.id}
|
|
||||||
document = {'$set': {'next_run_time': job.next_run_time},
|
|
||||||
'$inc': {'runs': 1}}
|
|
||||||
self.collection.update(spec, document)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.connection.disconnect()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
connection = self.collection.database.connection
|
|
||||||
return '<%s (connection=%s)>' % (self.__class__.__name__, connection)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
Stores jobs in an array in RAM. Provides no persistence support.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import JobStore
|
|
||||||
|
|
||||||
|
|
||||||
class RAMJobStore(JobStore):
|
|
||||||
def __init__(self):
|
|
||||||
self.jobs = []
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
self.jobs.append(job)
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def remove_job(self, job):
|
|
||||||
self.jobs.remove(job)
|
|
||||||
|
|
||||||
def load_jobs(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s>' % (self.__class__.__name__)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
"""
|
|
||||||
Stores jobs in a file governed by the :mod:`shelve` module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import shelve
|
|
||||||
import pickle
|
|
||||||
import random
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import JobStore
|
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.util import itervalues
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ShelveJobStore(JobStore):
|
|
||||||
MAX_ID = 1000000
|
|
||||||
|
|
||||||
def __init__(self, path, pickle_protocol=pickle.HIGHEST_PROTOCOL):
|
|
||||||
self.jobs = []
|
|
||||||
self.path = path
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
self.store = shelve.open(path, 'c', self.pickle_protocol)
|
|
||||||
|
|
||||||
def _generate_id(self):
|
|
||||||
id = None
|
|
||||||
while not id:
|
|
||||||
id = str(random.randint(1, self.MAX_ID))
|
|
||||||
if not id in self.store:
|
|
||||||
return id
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
job.id = self._generate_id()
|
|
||||||
self.jobs.append(job)
|
|
||||||
self.store[job.id] = job.__getstate__()
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
job_dict = self.store[job.id]
|
|
||||||
job_dict['next_run_time'] = job.next_run_time
|
|
||||||
job_dict['runs'] = job.runs
|
|
||||||
self.store[job.id] = job_dict
|
|
||||||
|
|
||||||
def remove_job(self, job):
|
|
||||||
del self.store[job.id]
|
|
||||||
self.jobs.remove(job)
|
|
||||||
|
|
||||||
def load_jobs(self):
|
|
||||||
jobs = []
|
|
||||||
for job_dict in itervalues(self.store):
|
|
||||||
try:
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job.__setstate__(job_dict)
|
|
||||||
jobs.append(job)
|
|
||||||
except Exception:
|
|
||||||
job_name = job_dict.get('name', '(unknown)')
|
|
||||||
logger.exception('Unable to restore job "%s"', job_name)
|
|
||||||
|
|
||||||
self.jobs = jobs
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.store.close()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (path=%s)>' % (self.__class__.__name__, self.path)
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
"""
|
|
||||||
Stores jobs in a database table using SQLAlchemy.
|
|
||||||
"""
|
|
||||||
import pickle
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import JobStore
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
from sqlalchemy import *
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed')
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyJobStore(JobStore):
|
|
||||||
def __init__(self, url=None, engine=None, tablename='apscheduler_jobs',
|
|
||||||
metadata=None, pickle_protocol=pickle.HIGHEST_PROTOCOL):
|
|
||||||
self.jobs = []
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
|
|
||||||
if engine:
|
|
||||||
self.engine = engine
|
|
||||||
elif url:
|
|
||||||
self.engine = create_engine(url)
|
|
||||||
else:
|
|
||||||
raise ValueError('Need either "engine" or "url" defined')
|
|
||||||
|
|
||||||
self.jobs_t = Table(tablename, metadata or MetaData(),
|
|
||||||
Column('id', Integer,
|
|
||||||
Sequence(tablename + '_id_seq', optional=True),
|
|
||||||
primary_key=True),
|
|
||||||
Column('trigger', PickleType(pickle_protocol, mutable=False),
|
|
||||||
nullable=False),
|
|
||||||
Column('func_ref', String(1024), nullable=False),
|
|
||||||
Column('args', PickleType(pickle_protocol, mutable=False),
|
|
||||||
nullable=False),
|
|
||||||
Column('kwargs', PickleType(pickle_protocol, mutable=False),
|
|
||||||
nullable=False),
|
|
||||||
Column('name', Unicode(1024), unique=True),
|
|
||||||
Column('misfire_grace_time', Integer, nullable=False),
|
|
||||||
Column('coalesce', Boolean, nullable=False),
|
|
||||||
Column('max_runs', Integer),
|
|
||||||
Column('max_instances', Integer),
|
|
||||||
Column('next_run_time', DateTime, nullable=False),
|
|
||||||
Column('runs', BigInteger))
|
|
||||||
|
|
||||||
self.jobs_t.create(self.engine, True)
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
job_dict = job.__getstate__()
|
|
||||||
result = self.engine.execute(self.jobs_t.insert().values(**job_dict))
|
|
||||||
job.id = result.inserted_primary_key[0]
|
|
||||||
self.jobs.append(job)
|
|
||||||
|
|
||||||
def remove_job(self, job):
|
|
||||||
delete = self.jobs_t.delete().where(self.jobs_t.c.id == job.id)
|
|
||||||
self.engine.execute(delete)
|
|
||||||
self.jobs.remove(job)
|
|
||||||
|
|
||||||
def load_jobs(self):
|
|
||||||
jobs = []
|
|
||||||
for row in self.engine.execute(select([self.jobs_t])):
|
|
||||||
try:
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job_dict = dict(row.items())
|
|
||||||
job.__setstate__(job_dict)
|
|
||||||
jobs.append(job)
|
|
||||||
except Exception:
|
|
||||||
job_name = job_dict.get('name', '(unknown)')
|
|
||||||
logger.exception('Unable to restore job "%s"', job_name)
|
|
||||||
self.jobs = jobs
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
job_dict = job.__getstate__()
|
|
||||||
update = self.jobs_t.update().where(self.jobs_t.c.id == job.id).\
|
|
||||||
values(next_run_time=job_dict['next_run_time'],
|
|
||||||
runs=job_dict['runs'])
|
|
||||||
self.engine.execute(update)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.engine.dispose()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (url=%s)>' % (self.__class__.__name__, self.engine.url)
|
|
||||||
@@ -1,559 +0,0 @@
|
|||||||
"""
|
|
||||||
This module is the main part of the library. It houses the Scheduler class
|
|
||||||
and related exceptions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from threading import Thread, Event, Lock
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from logging import getLogger
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from apscheduler.util import *
|
|
||||||
from apscheduler.triggers import SimpleTrigger, IntervalTrigger, CronTrigger
|
|
||||||
from apscheduler.jobstores.ram_store import RAMJobStore
|
|
||||||
from apscheduler.job import Job, MaxInstancesReachedError
|
|
||||||
from apscheduler.events import *
|
|
||||||
from apscheduler.threadpool import ThreadPool
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerAlreadyRunningError(Exception):
|
|
||||||
"""
|
|
||||||
Raised when attempting to start or configure the scheduler when it's
|
|
||||||
already running.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Scheduler is already running'
|
|
||||||
|
|
||||||
|
|
||||||
class Scheduler(object):
|
|
||||||
"""
|
|
||||||
This class is responsible for scheduling jobs and triggering
|
|
||||||
their execution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_stopped = False
|
|
||||||
_thread = None
|
|
||||||
|
|
||||||
def __init__(self, gconfig={}, **options):
|
|
||||||
self._wakeup = Event()
|
|
||||||
self._jobstores = {}
|
|
||||||
self._jobstores_lock = Lock()
|
|
||||||
self._listeners = []
|
|
||||||
self._listeners_lock = Lock()
|
|
||||||
self._pending_jobs = []
|
|
||||||
self.configure(gconfig, **options)
|
|
||||||
|
|
||||||
def configure(self, gconfig={}, **options):
|
|
||||||
"""
|
|
||||||
Reconfigures the scheduler with the given options. Can only be done
|
|
||||||
when the scheduler isn't running.
|
|
||||||
"""
|
|
||||||
if self.running:
|
|
||||||
raise SchedulerAlreadyRunningError
|
|
||||||
|
|
||||||
# Set general options
|
|
||||||
config = combine_opts(gconfig, 'apscheduler.', options)
|
|
||||||
self.misfire_grace_time = int(config.pop('misfire_grace_time', 1))
|
|
||||||
self.coalesce = asbool(config.pop('coalesce', True))
|
|
||||||
self.daemonic = asbool(config.pop('daemonic', True))
|
|
||||||
|
|
||||||
# Configure the thread pool
|
|
||||||
if 'threadpool' in config:
|
|
||||||
self._threadpool = maybe_ref(config['threadpool'])
|
|
||||||
else:
|
|
||||||
threadpool_opts = combine_opts(config, 'threadpool.')
|
|
||||||
self._threadpool = ThreadPool(**threadpool_opts)
|
|
||||||
|
|
||||||
# Configure job stores
|
|
||||||
jobstore_opts = combine_opts(config, 'jobstore.')
|
|
||||||
jobstores = {}
|
|
||||||
for key, value in jobstore_opts.items():
|
|
||||||
store_name, option = key.split('.', 1)
|
|
||||||
opts_dict = jobstores.setdefault(store_name, {})
|
|
||||||
opts_dict[option] = value
|
|
||||||
|
|
||||||
for alias, opts in jobstores.items():
|
|
||||||
classname = opts.pop('class')
|
|
||||||
cls = maybe_ref(classname)
|
|
||||||
jobstore = cls(**opts)
|
|
||||||
self.add_jobstore(jobstore, alias, True)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""
|
|
||||||
Starts the scheduler in a new thread.
|
|
||||||
"""
|
|
||||||
if self.running:
|
|
||||||
raise SchedulerAlreadyRunningError
|
|
||||||
|
|
||||||
# Create a RAMJobStore as the default if there is no default job store
|
|
||||||
if not 'default' in self._jobstores:
|
|
||||||
self.add_jobstore(RAMJobStore(), 'default', True)
|
|
||||||
|
|
||||||
# Schedule all pending jobs
|
|
||||||
for job, jobstore in self._pending_jobs:
|
|
||||||
self._real_add_job(job, jobstore, False)
|
|
||||||
del self._pending_jobs[:]
|
|
||||||
|
|
||||||
self._stopped = False
|
|
||||||
self._thread = Thread(target=self._main_loop, name='APScheduler')
|
|
||||||
self._thread.setDaemon(self.daemonic)
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def shutdown(self, wait=True, shutdown_threadpool=True):
|
|
||||||
"""
|
|
||||||
Shuts down the scheduler and terminates the thread.
|
|
||||||
Does not interrupt any currently running jobs.
|
|
||||||
|
|
||||||
:param wait: ``True`` to wait until all currently executing jobs have
|
|
||||||
finished (if ``shutdown_threadpool`` is also ``True``)
|
|
||||||
:param shutdown_threadpool: ``True`` to shut down the thread pool
|
|
||||||
"""
|
|
||||||
if not self.running:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._stopped = True
|
|
||||||
self._wakeup.set()
|
|
||||||
|
|
||||||
# Shut down the thread pool
|
|
||||||
if shutdown_threadpool:
|
|
||||||
self._threadpool.shutdown(wait)
|
|
||||||
|
|
||||||
# Wait until the scheduler thread terminates
|
|
||||||
self._thread.join()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def running(self):
|
|
||||||
return not self._stopped and self._thread and self._thread.isAlive()
|
|
||||||
|
|
||||||
def add_jobstore(self, jobstore, alias, quiet=False):
|
|
||||||
"""
|
|
||||||
Adds a job store to this scheduler.
|
|
||||||
|
|
||||||
:param jobstore: job store to be added
|
|
||||||
:param alias: alias for the job store
|
|
||||||
:param quiet: True to suppress scheduler thread wakeup
|
|
||||||
:type jobstore: instance of
|
|
||||||
:class:`~apscheduler.jobstores.base.JobStore`
|
|
||||||
:type alias: str
|
|
||||||
"""
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
if alias in self._jobstores:
|
|
||||||
raise KeyError('Alias "%s" is already in use' % alias)
|
|
||||||
self._jobstores[alias] = jobstore
|
|
||||||
jobstore.load_jobs()
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
# Notify listeners that a new job store has been added
|
|
||||||
self._notify_listeners(JobStoreEvent(EVENT_JOBSTORE_ADDED, alias))
|
|
||||||
|
|
||||||
# Notify the scheduler so it can scan the new job store for jobs
|
|
||||||
if not quiet:
|
|
||||||
self._wakeup.set()
|
|
||||||
|
|
||||||
def remove_jobstore(self, alias):
|
|
||||||
"""
|
|
||||||
Removes the job store by the given alias from this scheduler.
|
|
||||||
|
|
||||||
:type alias: str
|
|
||||||
"""
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
del self._jobstores[alias]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError('No such job store: %s' % alias)
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
# Notify listeners that a job store has been removed
|
|
||||||
self._notify_listeners(JobStoreEvent(EVENT_JOBSTORE_REMOVED, alias))
|
|
||||||
|
|
||||||
def add_listener(self, callback, mask=EVENT_ALL):
|
|
||||||
"""
|
|
||||||
Adds a listener for scheduler events. When a matching event occurs,
|
|
||||||
``callback`` is executed with the event object as its sole argument.
|
|
||||||
If the ``mask`` parameter is not provided, the callback will receive
|
|
||||||
events of all types.
|
|
||||||
|
|
||||||
:param callback: any callable that takes one argument
|
|
||||||
:param mask: bitmask that indicates which events should be listened to
|
|
||||||
"""
|
|
||||||
self._listeners_lock.acquire()
|
|
||||||
try:
|
|
||||||
self._listeners.append((callback, mask))
|
|
||||||
finally:
|
|
||||||
self._listeners_lock.release()
|
|
||||||
|
|
||||||
def remove_listener(self, callback):
|
|
||||||
"""
|
|
||||||
Removes a previously added event listener.
|
|
||||||
"""
|
|
||||||
self._listeners_lock.acquire()
|
|
||||||
try:
|
|
||||||
for i, (cb, _) in enumerate(self._listeners):
|
|
||||||
if callback == cb:
|
|
||||||
del self._listeners[i]
|
|
||||||
finally:
|
|
||||||
self._listeners_lock.release()
|
|
||||||
|
|
||||||
def _notify_listeners(self, event):
|
|
||||||
self._listeners_lock.acquire()
|
|
||||||
try:
|
|
||||||
listeners = tuple(self._listeners)
|
|
||||||
finally:
|
|
||||||
self._listeners_lock.release()
|
|
||||||
|
|
||||||
for cb, mask in listeners:
|
|
||||||
if event.code & mask:
|
|
||||||
try:
|
|
||||||
cb(event)
|
|
||||||
except:
|
|
||||||
logger.exception('Error notifying listener')
|
|
||||||
|
|
||||||
def _real_add_job(self, job, jobstore, wakeup):
|
|
||||||
job.compute_next_run_time(datetime.now())
|
|
||||||
if not job.next_run_time:
|
|
||||||
raise ValueError('Not adding job since it would never be run')
|
|
||||||
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
store = self._jobstores[jobstore]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError('No such job store: %s' % jobstore)
|
|
||||||
store.add_job(job)
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
# Notify listeners that a new job has been added
|
|
||||||
event = JobStoreEvent(EVENT_JOBSTORE_JOB_ADDED, jobstore, job)
|
|
||||||
self._notify_listeners(event)
|
|
||||||
|
|
||||||
logger.info('Added job "%s" to job store "%s"', job, jobstore)
|
|
||||||
|
|
||||||
# Notify the scheduler about the new job
|
|
||||||
if wakeup:
|
|
||||||
self._wakeup.set()
|
|
||||||
|
|
||||||
def add_job(self, trigger, func, args, kwargs, jobstore='default',
|
|
||||||
**options):
|
|
||||||
"""
|
|
||||||
Adds the given job to the job list and notifies the scheduler thread.
|
|
||||||
|
|
||||||
:param trigger: alias of the job store to store the job in
|
|
||||||
:param func: callable to run at the given time
|
|
||||||
:param args: list of positional arguments to call func with
|
|
||||||
:param kwargs: dict of keyword arguments to call func with
|
|
||||||
:param jobstore: alias of the job store to store the job in
|
|
||||||
:rtype: :class:`~apscheduler.job.Job`
|
|
||||||
"""
|
|
||||||
job = Job(trigger, func, args or [], kwargs or {},
|
|
||||||
options.pop('misfire_grace_time', self.misfire_grace_time),
|
|
||||||
options.pop('coalesce', self.coalesce), **options)
|
|
||||||
if not self.running:
|
|
||||||
self._pending_jobs.append((job, jobstore))
|
|
||||||
logger.info('Adding job tentatively -- it will be properly '
|
|
||||||
'scheduled when the scheduler starts')
|
|
||||||
else:
|
|
||||||
self._real_add_job(job, jobstore, True)
|
|
||||||
return job
|
|
||||||
|
|
||||||
def _remove_job(self, job, alias, jobstore):
|
|
||||||
jobstore.remove_job(job)
|
|
||||||
|
|
||||||
# Notify listeners that a job has been removed
|
|
||||||
event = JobStoreEvent(EVENT_JOBSTORE_JOB_REMOVED, alias, job)
|
|
||||||
self._notify_listeners(event)
|
|
||||||
|
|
||||||
logger.info('Removed job "%s"', job)
|
|
||||||
|
|
||||||
def add_date_job(self, func, date, args=None, kwargs=None, **options):
|
|
||||||
"""
|
|
||||||
Schedules a job to be completed on a specific date and time.
|
|
||||||
|
|
||||||
:param func: callable to run at the given time
|
|
||||||
:param date: the date/time to run the job at
|
|
||||||
:param name: name of the job
|
|
||||||
:param jobstore: stored the job in the named (or given) job store
|
|
||||||
:param misfire_grace_time: seconds after the designated run time that
|
|
||||||
the job is still allowed to be run
|
|
||||||
:type date: :class:`datetime.date`
|
|
||||||
:rtype: :class:`~apscheduler.job.Job`
|
|
||||||
"""
|
|
||||||
trigger = SimpleTrigger(date)
|
|
||||||
return self.add_job(trigger, func, args, kwargs, **options)
|
|
||||||
|
|
||||||
def add_interval_job(self, func, weeks=0, days=0, hours=0, minutes=0,
|
|
||||||
seconds=0, start_date=None, args=None, kwargs=None,
|
|
||||||
**options):
|
|
||||||
"""
|
|
||||||
Schedules a job to be completed on specified intervals.
|
|
||||||
|
|
||||||
:param func: callable to run
|
|
||||||
:param weeks: number of weeks to wait
|
|
||||||
:param days: number of days to wait
|
|
||||||
:param hours: number of hours to wait
|
|
||||||
:param minutes: number of minutes to wait
|
|
||||||
:param seconds: number of seconds to wait
|
|
||||||
:param start_date: when to first execute the job and start the
|
|
||||||
counter (default is after the given interval)
|
|
||||||
:param args: list of positional arguments to call func with
|
|
||||||
:param kwargs: dict of keyword arguments to call func with
|
|
||||||
:param name: name of the job
|
|
||||||
:param jobstore: alias of the job store to add the job to
|
|
||||||
:param misfire_grace_time: seconds after the designated run time that
|
|
||||||
the job is still allowed to be run
|
|
||||||
:rtype: :class:`~apscheduler.job.Job`
|
|
||||||
"""
|
|
||||||
interval = timedelta(weeks=weeks, days=days, hours=hours,
|
|
||||||
minutes=minutes, seconds=seconds)
|
|
||||||
trigger = IntervalTrigger(interval, start_date)
|
|
||||||
return self.add_job(trigger, func, args, kwargs, **options)
|
|
||||||
|
|
||||||
def add_cron_job(self, func, year='*', month='*', day='*', week='*',
|
|
||||||
day_of_week='*', hour='*', minute='*', second='*',
|
|
||||||
start_date=None, args=None, kwargs=None, **options):
|
|
||||||
"""
|
|
||||||
Schedules a job to be completed on times that match the given
|
|
||||||
expressions.
|
|
||||||
|
|
||||||
:param func: callable to run
|
|
||||||
:param year: year to run on
|
|
||||||
:param month: month to run on (0 = January)
|
|
||||||
:param day: day of month to run on
|
|
||||||
:param week: week of the year to run on
|
|
||||||
:param day_of_week: weekday to run on (0 = Monday)
|
|
||||||
:param hour: hour to run on
|
|
||||||
:param second: second to run on
|
|
||||||
:param args: list of positional arguments to call func with
|
|
||||||
:param kwargs: dict of keyword arguments to call func with
|
|
||||||
:param name: name of the job
|
|
||||||
:param jobstore: alias of the job store to add the job to
|
|
||||||
:param misfire_grace_time: seconds after the designated run time that
|
|
||||||
the job is still allowed to be run
|
|
||||||
:return: the scheduled job
|
|
||||||
:rtype: :class:`~apscheduler.job.Job`
|
|
||||||
"""
|
|
||||||
trigger = CronTrigger(year=year, month=month, day=day, week=week,
|
|
||||||
day_of_week=day_of_week, hour=hour,
|
|
||||||
minute=minute, second=second,
|
|
||||||
start_date=start_date)
|
|
||||||
return self.add_job(trigger, func, args, kwargs, **options)
|
|
||||||
|
|
||||||
def cron_schedule(self, **options):
|
|
||||||
"""
|
|
||||||
Decorator version of :meth:`add_cron_job`.
|
|
||||||
This decorator does not wrap its host function.
|
|
||||||
Unscheduling decorated functions is possible by passing the ``job``
|
|
||||||
attribute of the scheduled function to :meth:`unschedule_job`.
|
|
||||||
"""
|
|
||||||
def inner(func):
|
|
||||||
func.job = self.add_cron_job(func, **options)
|
|
||||||
return func
|
|
||||||
return inner
|
|
||||||
|
|
||||||
def interval_schedule(self, **options):
|
|
||||||
"""
|
|
||||||
Decorator version of :meth:`add_interval_job`.
|
|
||||||
This decorator does not wrap its host function.
|
|
||||||
Unscheduling decorated functions is possible by passing the ``job``
|
|
||||||
attribute of the scheduled function to :meth:`unschedule_job`.
|
|
||||||
"""
|
|
||||||
def inner(func):
|
|
||||||
func.job = self.add_interval_job(func, **options)
|
|
||||||
return func
|
|
||||||
return inner
|
|
||||||
|
|
||||||
def get_jobs(self):
|
|
||||||
"""
|
|
||||||
Returns a list of all scheduled jobs.
|
|
||||||
|
|
||||||
:return: list of :class:`~apscheduler.job.Job` objects
|
|
||||||
"""
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
jobs = []
|
|
||||||
for jobstore in itervalues(self._jobstores):
|
|
||||||
jobs.extend(jobstore.jobs)
|
|
||||||
return jobs
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
def unschedule_job(self, job):
|
|
||||||
"""
|
|
||||||
Removes a job, preventing it from being run any more.
|
|
||||||
"""
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
for alias, jobstore in iteritems(self._jobstores):
|
|
||||||
if job in list(jobstore.jobs):
|
|
||||||
self._remove_job(job, alias, jobstore)
|
|
||||||
return
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
raise KeyError('Job "%s" is not scheduled in any job store' % job)
|
|
||||||
|
|
||||||
def unschedule_func(self, func):
|
|
||||||
"""
|
|
||||||
Removes all jobs that would execute the given function.
|
|
||||||
"""
|
|
||||||
found = False
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
for alias, jobstore in iteritems(self._jobstores):
|
|
||||||
for job in list(jobstore.jobs):
|
|
||||||
if job.func == func:
|
|
||||||
self._remove_job(job, alias, jobstore)
|
|
||||||
found = True
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
raise KeyError('The given function is not scheduled in this '
|
|
||||||
'scheduler')
|
|
||||||
|
|
||||||
def print_jobs(self, out=None):
|
|
||||||
"""
|
|
||||||
Prints out a textual listing of all jobs currently scheduled on this
|
|
||||||
scheduler.
|
|
||||||
|
|
||||||
:param out: a file-like object to print to (defaults to **sys.stdout**
|
|
||||||
if nothing is given)
|
|
||||||
"""
|
|
||||||
out = out or sys.stdout
|
|
||||||
job_strs = []
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
for alias, jobstore in iteritems(self._jobstores):
|
|
||||||
job_strs.append('Jobstore %s:' % alias)
|
|
||||||
if jobstore.jobs:
|
|
||||||
for job in jobstore.jobs:
|
|
||||||
job_strs.append(' %s' % job)
|
|
||||||
else:
|
|
||||||
job_strs.append(' No scheduled jobs')
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
out.write(os.linesep.join(job_strs))
|
|
||||||
|
|
||||||
def _run_job(self, job, run_times):
|
|
||||||
"""
|
|
||||||
Acts as a harness that runs the actual job code in a thread.
|
|
||||||
"""
|
|
||||||
for run_time in run_times:
|
|
||||||
# See if the job missed its run time window, and handle possible
|
|
||||||
# misfires accordingly
|
|
||||||
difference = datetime.now() - run_time
|
|
||||||
grace_time = timedelta(seconds=job.misfire_grace_time)
|
|
||||||
if difference > grace_time:
|
|
||||||
# Notify listeners about a missed run
|
|
||||||
event = JobEvent(EVENT_JOB_MISSED, job, run_time)
|
|
||||||
self._notify_listeners(event)
|
|
||||||
logger.warning('Run time of job "%s" was missed by %s',
|
|
||||||
job, difference)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
job.add_instance()
|
|
||||||
except MaxInstancesReachedError:
|
|
||||||
event = JobEvent(EVENT_JOB_MISSED, job, run_time)
|
|
||||||
self._notify_listeners(event)
|
|
||||||
logger.warning('Execution of job "%s" skipped: '
|
|
||||||
'maximum number of running instances '
|
|
||||||
'reached (%d)', job, job.max_instances)
|
|
||||||
break
|
|
||||||
|
|
||||||
logger.info('Running job "%s" (scheduled at %s)', job,
|
|
||||||
run_time)
|
|
||||||
|
|
||||||
try:
|
|
||||||
retval = job.func(*job.args, **job.kwargs)
|
|
||||||
except:
|
|
||||||
# Notify listeners about the exception
|
|
||||||
exc, tb = sys.exc_info()[1:]
|
|
||||||
event = JobEvent(EVENT_JOB_ERROR, job, run_time,
|
|
||||||
exception=exc, traceback=tb)
|
|
||||||
self._notify_listeners(event)
|
|
||||||
|
|
||||||
logger.exception('Job "%s" raised an exception', job)
|
|
||||||
else:
|
|
||||||
# Notify listeners about successful execution
|
|
||||||
event = JobEvent(EVENT_JOB_EXECUTED, job, run_time,
|
|
||||||
retval=retval)
|
|
||||||
self._notify_listeners(event)
|
|
||||||
|
|
||||||
logger.info('Job "%s" executed successfully', job)
|
|
||||||
|
|
||||||
job.remove_instance()
|
|
||||||
|
|
||||||
# If coalescing is enabled, don't attempt any further runs
|
|
||||||
if job.coalesce:
|
|
||||||
break
|
|
||||||
|
|
||||||
def _process_jobs(self, now):
|
|
||||||
"""
|
|
||||||
Iterates through jobs in every jobstore, starts pending jobs
|
|
||||||
and figures out the next wakeup time.
|
|
||||||
"""
|
|
||||||
next_wakeup_time = None
|
|
||||||
self._jobstores_lock.acquire()
|
|
||||||
try:
|
|
||||||
for alias, jobstore in iteritems(self._jobstores):
|
|
||||||
for job in tuple(jobstore.jobs):
|
|
||||||
run_times = job.get_run_times(now)
|
|
||||||
if run_times:
|
|
||||||
self._threadpool.submit(self._run_job, job, run_times)
|
|
||||||
|
|
||||||
# Increase the job's run count
|
|
||||||
if job.coalesce:
|
|
||||||
job.runs += 1
|
|
||||||
else:
|
|
||||||
job.runs += len(run_times)
|
|
||||||
|
|
||||||
# Update the job, but don't keep finished jobs around
|
|
||||||
if job.compute_next_run_time(now + timedelta(microseconds=1)):
|
|
||||||
jobstore.update_job(job)
|
|
||||||
else:
|
|
||||||
self._remove_job(job, alias, jobstore)
|
|
||||||
|
|
||||||
if not next_wakeup_time:
|
|
||||||
next_wakeup_time = job.next_run_time
|
|
||||||
elif job.next_run_time:
|
|
||||||
next_wakeup_time = min(next_wakeup_time,
|
|
||||||
job.next_run_time)
|
|
||||||
return next_wakeup_time
|
|
||||||
finally:
|
|
||||||
self._jobstores_lock.release()
|
|
||||||
|
|
||||||
def _main_loop(self):
|
|
||||||
"""Executes jobs on schedule."""
|
|
||||||
|
|
||||||
logger.info('Scheduler started')
|
|
||||||
self._notify_listeners(SchedulerEvent(EVENT_SCHEDULER_START))
|
|
||||||
|
|
||||||
self._wakeup.clear()
|
|
||||||
while not self._stopped:
|
|
||||||
logger.debug('Looking for jobs to run')
|
|
||||||
now = datetime.now()
|
|
||||||
next_wakeup_time = self._process_jobs(now)
|
|
||||||
|
|
||||||
# Sleep until the next job is scheduled to be run,
|
|
||||||
# a new job is added or the scheduler is stopped
|
|
||||||
if next_wakeup_time is not None:
|
|
||||||
wait_seconds = time_difference(next_wakeup_time, now)
|
|
||||||
logger.debug('Next wakeup is due at %s (in %f seconds)',
|
|
||||||
next_wakeup_time, wait_seconds)
|
|
||||||
self._wakeup.wait(wait_seconds)
|
|
||||||
else:
|
|
||||||
logger.debug('No jobs; waiting until a job is added')
|
|
||||||
self._wakeup.wait()
|
|
||||||
self._wakeup.clear()
|
|
||||||
|
|
||||||
logger.info('Scheduler has been shut down')
|
|
||||||
self._notify_listeners(SchedulerEvent(EVENT_SCHEDULER_SHUTDOWN))
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
"""
|
|
||||||
Generic thread pool class. Modeled after Java's ThreadPoolExecutor.
|
|
||||||
Please note that this ThreadPool does *not* fully implement the PEP 3148
|
|
||||||
ThreadPool!
|
|
||||||
"""
|
|
||||||
|
|
||||||
from threading import Thread, Lock, currentThread
|
|
||||||
from weakref import ref
|
|
||||||
import logging
|
|
||||||
import atexit
|
|
||||||
|
|
||||||
try:
|
|
||||||
from queue import Queue, Empty
|
|
||||||
except ImportError:
|
|
||||||
from Queue import Queue, Empty
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
_threadpools = set()
|
|
||||||
|
|
||||||
|
|
||||||
# Worker threads are daemonic in order to let the interpreter exit without
|
|
||||||
# an explicit shutdown of the thread pool. The following trick is necessary
|
|
||||||
# to allow worker threads to finish cleanly.
|
|
||||||
def _shutdown_all():
|
|
||||||
for pool_ref in tuple(_threadpools):
|
|
||||||
pool = pool_ref()
|
|
||||||
if pool:
|
|
||||||
pool.shutdown()
|
|
||||||
|
|
||||||
atexit.register(_shutdown_all)
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadPool(object):
|
|
||||||
def __init__(self, core_threads=0, max_threads=20, keepalive=1):
|
|
||||||
"""
|
|
||||||
:param core_threads: maximum number of persistent threads in the pool
|
|
||||||
:param max_threads: maximum number of total threads in the pool
|
|
||||||
:param thread_class: callable that creates a Thread object
|
|
||||||
:param keepalive: seconds to keep non-core worker threads waiting
|
|
||||||
for new tasks
|
|
||||||
"""
|
|
||||||
self.core_threads = core_threads
|
|
||||||
self.max_threads = max(max_threads, core_threads, 1)
|
|
||||||
self.keepalive = keepalive
|
|
||||||
self._queue = Queue()
|
|
||||||
self._threads_lock = Lock()
|
|
||||||
self._threads = set()
|
|
||||||
self._shutdown = False
|
|
||||||
|
|
||||||
_threadpools.add(ref(self))
|
|
||||||
logger.info('Started thread pool with %d core threads and %s maximum '
|
|
||||||
'threads', core_threads, max_threads or 'unlimited')
|
|
||||||
|
|
||||||
def _adjust_threadcount(self):
|
|
||||||
self._threads_lock.acquire()
|
|
||||||
try:
|
|
||||||
if self.num_threads < self.max_threads:
|
|
||||||
self._add_thread(self.num_threads < self.core_threads)
|
|
||||||
finally:
|
|
||||||
self._threads_lock.release()
|
|
||||||
|
|
||||||
def _add_thread(self, core):
|
|
||||||
t = Thread(target=self._run_jobs, args=(core,))
|
|
||||||
t.setDaemon(True)
|
|
||||||
t.start()
|
|
||||||
self._threads.add(t)
|
|
||||||
|
|
||||||
def _run_jobs(self, core):
|
|
||||||
logger.debug('Started worker thread')
|
|
||||||
block = True
|
|
||||||
timeout = None
|
|
||||||
if not core:
|
|
||||||
block = self.keepalive > 0
|
|
||||||
timeout = self.keepalive
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
func, args, kwargs = self._queue.get(block, timeout)
|
|
||||||
except Empty:
|
|
||||||
break
|
|
||||||
|
|
||||||
if self._shutdown:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
func(*args, **kwargs)
|
|
||||||
except:
|
|
||||||
logger.exception('Error in worker thread')
|
|
||||||
|
|
||||||
self._threads_lock.acquire()
|
|
||||||
self._threads.remove(currentThread())
|
|
||||||
self._threads_lock.release()
|
|
||||||
|
|
||||||
logger.debug('Exiting worker thread')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def num_threads(self):
|
|
||||||
return len(self._threads)
|
|
||||||
|
|
||||||
def submit(self, func, *args, **kwargs):
|
|
||||||
if self._shutdown:
|
|
||||||
raise RuntimeError('Cannot schedule new tasks after shutdown')
|
|
||||||
|
|
||||||
self._queue.put((func, args, kwargs))
|
|
||||||
self._adjust_threadcount()
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
if self._shutdown:
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.info('Shutting down thread pool')
|
|
||||||
self._shutdown = True
|
|
||||||
_threadpools.remove(ref(self))
|
|
||||||
|
|
||||||
self._threads_lock.acquire()
|
|
||||||
for _ in range(self.num_threads):
|
|
||||||
self._queue.put((None, None, None))
|
|
||||||
self._threads_lock.release()
|
|
||||||
|
|
||||||
if wait:
|
|
||||||
self._threads_lock.acquire()
|
|
||||||
threads = tuple(self._threads)
|
|
||||||
self._threads_lock.release()
|
|
||||||
for thread in threads:
|
|
||||||
thread.join()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.max_threads:
|
|
||||||
threadcount = '%d/%d' % (self.num_threads, self.max_threads)
|
|
||||||
else:
|
|
||||||
threadcount = '%d' % self.num_threads
|
|
||||||
|
|
||||||
return '<ThreadPool at %x; threads=%s>' % (id(self), threadcount)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from apscheduler.triggers.cron import CronTrigger
|
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
|
||||||
from apscheduler.triggers.simple import SimpleTrigger
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
from datetime import date, datetime
|
|
||||||
|
|
||||||
from apscheduler.triggers.cron.fields import *
|
|
||||||
from apscheduler.util import datetime_ceil, convert_to_datetime
|
|
||||||
|
|
||||||
|
|
||||||
class CronTrigger(object):
|
|
||||||
FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour',
|
|
||||||
'minute', 'second')
|
|
||||||
FIELDS_MAP = {'year': BaseField,
|
|
||||||
'month': BaseField,
|
|
||||||
'week': WeekField,
|
|
||||||
'day': DayOfMonthField,
|
|
||||||
'day_of_week': DayOfWeekField,
|
|
||||||
'hour': BaseField,
|
|
||||||
'minute': BaseField,
|
|
||||||
'second': BaseField}
|
|
||||||
|
|
||||||
def __init__(self, **values):
|
|
||||||
self.start_date = values.pop('start_date', None)
|
|
||||||
if self.start_date:
|
|
||||||
self.start_date = convert_to_datetime(self.start_date)
|
|
||||||
|
|
||||||
self.fields = []
|
|
||||||
for field_name in self.FIELD_NAMES:
|
|
||||||
if field_name in values:
|
|
||||||
exprs = values.pop(field_name)
|
|
||||||
is_default = False
|
|
||||||
elif not values:
|
|
||||||
exprs = DEFAULT_VALUES[field_name]
|
|
||||||
is_default = True
|
|
||||||
else:
|
|
||||||
exprs = '*'
|
|
||||||
is_default = True
|
|
||||||
|
|
||||||
field_class = self.FIELDS_MAP[field_name]
|
|
||||||
field = field_class(field_name, exprs, is_default)
|
|
||||||
self.fields.append(field)
|
|
||||||
|
|
||||||
def _increment_field_value(self, dateval, fieldnum):
|
|
||||||
"""
|
|
||||||
Increments the designated field and resets all less significant fields
|
|
||||||
to their minimum values.
|
|
||||||
|
|
||||||
:type dateval: datetime
|
|
||||||
:type fieldnum: int
|
|
||||||
:type amount: int
|
|
||||||
:rtype: tuple
|
|
||||||
:return: a tuple containing the new date, and the number of the field
|
|
||||||
that was actually incremented
|
|
||||||
"""
|
|
||||||
i = 0
|
|
||||||
values = {}
|
|
||||||
while i < len(self.fields):
|
|
||||||
field = self.fields[i]
|
|
||||||
if not field.REAL:
|
|
||||||
if i == fieldnum:
|
|
||||||
fieldnum -= 1
|
|
||||||
i -= 1
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if i < fieldnum:
|
|
||||||
values[field.name] = field.get_value(dateval)
|
|
||||||
i += 1
|
|
||||||
elif i > fieldnum:
|
|
||||||
values[field.name] = field.get_min(dateval)
|
|
||||||
i += 1
|
|
||||||
else:
|
|
||||||
value = field.get_value(dateval)
|
|
||||||
maxval = field.get_max(dateval)
|
|
||||||
if value == maxval:
|
|
||||||
fieldnum -= 1
|
|
||||||
i -= 1
|
|
||||||
else:
|
|
||||||
values[field.name] = value + 1
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return datetime(**values), fieldnum
|
|
||||||
|
|
||||||
def _set_field_value(self, dateval, fieldnum, new_value):
|
|
||||||
values = {}
|
|
||||||
for i, field in enumerate(self.fields):
|
|
||||||
if field.REAL:
|
|
||||||
if i < fieldnum:
|
|
||||||
values[field.name] = field.get_value(dateval)
|
|
||||||
elif i > fieldnum:
|
|
||||||
values[field.name] = field.get_min(dateval)
|
|
||||||
else:
|
|
||||||
values[field.name] = new_value
|
|
||||||
|
|
||||||
return datetime(**values)
|
|
||||||
|
|
||||||
def get_next_fire_time(self, start_date):
|
|
||||||
if self.start_date:
|
|
||||||
start_date = max(start_date, self.start_date)
|
|
||||||
next_date = datetime_ceil(start_date)
|
|
||||||
fieldnum = 0
|
|
||||||
while 0 <= fieldnum < len(self.fields):
|
|
||||||
field = self.fields[fieldnum]
|
|
||||||
curr_value = field.get_value(next_date)
|
|
||||||
next_value = field.get_next_value(next_date)
|
|
||||||
|
|
||||||
if next_value is None:
|
|
||||||
# No valid value was found
|
|
||||||
next_date, fieldnum = self._increment_field_value(next_date,
|
|
||||||
fieldnum - 1)
|
|
||||||
elif next_value > curr_value:
|
|
||||||
# A valid, but higher than the starting value, was found
|
|
||||||
if field.REAL:
|
|
||||||
next_date = self._set_field_value(next_date, fieldnum,
|
|
||||||
next_value)
|
|
||||||
fieldnum += 1
|
|
||||||
else:
|
|
||||||
next_date, fieldnum = self._increment_field_value(next_date,
|
|
||||||
fieldnum)
|
|
||||||
else:
|
|
||||||
# A valid value was found, no changes necessary
|
|
||||||
fieldnum += 1
|
|
||||||
|
|
||||||
if fieldnum >= 0:
|
|
||||||
return next_date
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
options = ["%s='%s'" % (f.name, str(f)) for f in self.fields
|
|
||||||
if not f.is_default]
|
|
||||||
return 'cron[%s]' % (', '.join(options))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
options = ["%s='%s'" % (f.name, str(f)) for f in self.fields
|
|
||||||
if not f.is_default]
|
|
||||||
if self.start_date:
|
|
||||||
options.append("start_date='%s'" % self.start_date.isoformat(' '))
|
|
||||||
return '<%s (%s)>' % (self.__class__.__name__, ', '.join(options))
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
"""
|
|
||||||
This module contains the expressions applicable for CronTrigger's fields.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from calendar import monthrange
|
|
||||||
import re
|
|
||||||
|
|
||||||
from apscheduler.util import asint
|
|
||||||
|
|
||||||
__all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
|
|
||||||
'WeekdayPositionExpression')
|
|
||||||
|
|
||||||
|
|
||||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
|
||||||
|
|
||||||
|
|
||||||
class AllExpression(object):
|
|
||||||
value_re = re.compile(r'\*(?:/(?P<step>\d+))?$')
|
|
||||||
|
|
||||||
def __init__(self, step=None):
|
|
||||||
self.step = asint(step)
|
|
||||||
if self.step == 0:
|
|
||||||
raise ValueError('Increment must be higher than 0')
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
start = field.get_value(date)
|
|
||||||
minval = field.get_min(date)
|
|
||||||
maxval = field.get_max(date)
|
|
||||||
start = max(start, minval)
|
|
||||||
|
|
||||||
if not self.step:
|
|
||||||
next = start
|
|
||||||
else:
|
|
||||||
distance_to_next = (self.step - (start - minval)) % self.step
|
|
||||||
next = start + distance_to_next
|
|
||||||
|
|
||||||
if next <= maxval:
|
|
||||||
return next
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.step:
|
|
||||||
return '*/%d' % self.step
|
|
||||||
return '*'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, self.step)
|
|
||||||
|
|
||||||
|
|
||||||
class RangeExpression(AllExpression):
|
|
||||||
value_re = re.compile(
|
|
||||||
r'(?P<first>\d+)(?:-(?P<last>\d+))?(?:/(?P<step>\d+))?$')
|
|
||||||
|
|
||||||
def __init__(self, first, last=None, step=None):
|
|
||||||
AllExpression.__init__(self, step)
|
|
||||||
first = asint(first)
|
|
||||||
last = asint(last)
|
|
||||||
if last is None and step is None:
|
|
||||||
last = first
|
|
||||||
if last is not None and first > last:
|
|
||||||
raise ValueError('The minimum value in a range must not be '
|
|
||||||
'higher than the maximum')
|
|
||||||
self.first = first
|
|
||||||
self.last = last
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
start = field.get_value(date)
|
|
||||||
minval = field.get_min(date)
|
|
||||||
maxval = field.get_max(date)
|
|
||||||
|
|
||||||
# Apply range limits
|
|
||||||
minval = max(minval, self.first)
|
|
||||||
if self.last is not None:
|
|
||||||
maxval = min(maxval, self.last)
|
|
||||||
start = max(start, minval)
|
|
||||||
|
|
||||||
if not self.step:
|
|
||||||
next = start
|
|
||||||
else:
|
|
||||||
distance_to_next = (self.step - (start - minval)) % self.step
|
|
||||||
next = start + distance_to_next
|
|
||||||
|
|
||||||
if next <= maxval:
|
|
||||||
return next
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
range = '%d-%d' % (self.first, self.last)
|
|
||||||
else:
|
|
||||||
range = str(self.first)
|
|
||||||
|
|
||||||
if self.step:
|
|
||||||
return '%s/%d' % (range, self.step)
|
|
||||||
return range
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = [str(self.first)]
|
|
||||||
if self.last != self.first and self.last is not None or self.step:
|
|
||||||
args.append(str(self.last))
|
|
||||||
if self.step:
|
|
||||||
args.append(str(self.step))
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
|
||||||
|
|
||||||
|
|
||||||
class WeekdayRangeExpression(RangeExpression):
|
|
||||||
value_re = re.compile(r'(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?',
|
|
||||||
re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self, first, last=None):
|
|
||||||
try:
|
|
||||||
first_num = WEEKDAYS.index(first.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday name "%s"' % first)
|
|
||||||
|
|
||||||
if last:
|
|
||||||
try:
|
|
||||||
last_num = WEEKDAYS.index(last.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday name "%s"' % last)
|
|
||||||
else:
|
|
||||||
last_num = None
|
|
||||||
|
|
||||||
RangeExpression.__init__(self, first_num, last_num)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last])
|
|
||||||
return WEEKDAYS[self.first]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = ["'%s'" % WEEKDAYS[self.first]]
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
args.append("'%s'" % WEEKDAYS[self.last])
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
|
||||||
|
|
||||||
|
|
||||||
class WeekdayPositionExpression(AllExpression):
|
|
||||||
options = ['1st', '2nd', '3rd', '4th', '5th', 'last']
|
|
||||||
value_re = re.compile(r'(?P<option_name>%s) +(?P<weekday_name>(?:\d+|\w+))'
|
|
||||||
% '|'.join(options), re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self, option_name, weekday_name):
|
|
||||||
try:
|
|
||||||
self.option_num = self.options.index(option_name.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday position "%s"' % option_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.weekday = WEEKDAYS.index(weekday_name.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday name "%s"' % weekday_name)
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
# Figure out the weekday of the month's first day and the number
|
|
||||||
# of days in that month
|
|
||||||
first_day_wday, last_day = monthrange(date.year, date.month)
|
|
||||||
|
|
||||||
# Calculate which day of the month is the first of the target weekdays
|
|
||||||
first_hit_day = self.weekday - first_day_wday + 1
|
|
||||||
if first_hit_day <= 0:
|
|
||||||
first_hit_day += 7
|
|
||||||
|
|
||||||
# Calculate what day of the month the target weekday would be
|
|
||||||
if self.option_num < 5:
|
|
||||||
target_day = first_hit_day + self.option_num * 7
|
|
||||||
else:
|
|
||||||
target_day = first_hit_day + ((last_day - first_hit_day) / 7) * 7
|
|
||||||
|
|
||||||
if target_day <= last_day and target_day >= date.day:
|
|
||||||
return target_day
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '%s %s' % (self.options[self.option_num],
|
|
||||||
WEEKDAYS[self.weekday])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s('%s', '%s')" % (self.__class__.__name__,
|
|
||||||
self.options[self.option_num],
|
|
||||||
WEEKDAYS[self.weekday])
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
"""
|
|
||||||
Fields represent CronTrigger options which map to :class:`~datetime.datetime`
|
|
||||||
fields.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from calendar import monthrange
|
|
||||||
|
|
||||||
from apscheduler.triggers.cron.expressions import *
|
|
||||||
|
|
||||||
__all__ = ('MIN_VALUES', 'MAX_VALUES', 'DEFAULT_VALUES', 'BaseField',
|
|
||||||
'WeekField', 'DayOfMonthField', 'DayOfWeekField')
|
|
||||||
|
|
||||||
|
|
||||||
MIN_VALUES = {'year': 1970, 'month': 1, 'day': 1, 'week': 1,
|
|
||||||
'day_of_week': 0, 'hour': 0, 'minute': 0, 'second': 0}
|
|
||||||
MAX_VALUES = {'year': 2 ** 63, 'month': 12, 'day:': 31, 'week': 53,
|
|
||||||
'day_of_week': 6, 'hour': 23, 'minute': 59, 'second': 59}
|
|
||||||
DEFAULT_VALUES = {'year': '*', 'month': 1, 'day': 1, 'week': '*',
|
|
||||||
'day_of_week': '*', 'hour': 0, 'minute': 0, 'second': 0}
|
|
||||||
|
|
||||||
|
|
||||||
class BaseField(object):
|
|
||||||
REAL = True
|
|
||||||
COMPILERS = [AllExpression, RangeExpression]
|
|
||||||
|
|
||||||
def __init__(self, name, exprs, is_default=False):
|
|
||||||
self.name = name
|
|
||||||
self.is_default = is_default
|
|
||||||
self.compile_expressions(exprs)
|
|
||||||
|
|
||||||
def get_min(self, dateval):
|
|
||||||
return MIN_VALUES[self.name]
|
|
||||||
|
|
||||||
def get_max(self, dateval):
|
|
||||||
return MAX_VALUES[self.name]
|
|
||||||
|
|
||||||
def get_value(self, dateval):
|
|
||||||
return getattr(dateval, self.name)
|
|
||||||
|
|
||||||
def get_next_value(self, dateval):
|
|
||||||
smallest = None
|
|
||||||
for expr in self.expressions:
|
|
||||||
value = expr.get_next_value(dateval, self)
|
|
||||||
if smallest is None or (value is not None and value < smallest):
|
|
||||||
smallest = value
|
|
||||||
|
|
||||||
return smallest
|
|
||||||
|
|
||||||
def compile_expressions(self, exprs):
|
|
||||||
self.expressions = []
|
|
||||||
|
|
||||||
# Split a comma-separated expression list, if any
|
|
||||||
exprs = str(exprs).strip()
|
|
||||||
if ',' in exprs:
|
|
||||||
for expr in exprs.split(','):
|
|
||||||
self.compile_expression(expr)
|
|
||||||
else:
|
|
||||||
self.compile_expression(exprs)
|
|
||||||
|
|
||||||
def compile_expression(self, expr):
|
|
||||||
for compiler in self.COMPILERS:
|
|
||||||
match = compiler.value_re.match(expr)
|
|
||||||
if match:
|
|
||||||
compiled_expr = compiler(**match.groupdict())
|
|
||||||
self.expressions.append(compiled_expr)
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ValueError('Unrecognized expression "%s" for field "%s"' %
|
|
||||||
(expr, self.name))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
expr_strings = (str(e) for e in self.expressions)
|
|
||||||
return ','.join(expr_strings)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s('%s', '%s')" % (self.__class__.__name__, self.name,
|
|
||||||
str(self))
|
|
||||||
|
|
||||||
|
|
||||||
class WeekField(BaseField):
|
|
||||||
REAL = False
|
|
||||||
|
|
||||||
def get_value(self, dateval):
|
|
||||||
return dateval.isocalendar()[1]
|
|
||||||
|
|
||||||
|
|
||||||
class DayOfMonthField(BaseField):
|
|
||||||
COMPILERS = BaseField.COMPILERS + [WeekdayPositionExpression]
|
|
||||||
|
|
||||||
def get_max(self, dateval):
|
|
||||||
return monthrange(dateval.year, dateval.month)[1]
|
|
||||||
|
|
||||||
|
|
||||||
class DayOfWeekField(BaseField):
|
|
||||||
REAL = False
|
|
||||||
COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression]
|
|
||||||
|
|
||||||
def get_value(self, dateval):
|
|
||||||
return dateval.weekday()
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from math import ceil
|
|
||||||
|
|
||||||
from apscheduler.util import convert_to_datetime, timedelta_seconds
|
|
||||||
|
|
||||||
|
|
||||||
class IntervalTrigger(object):
|
|
||||||
def __init__(self, interval, start_date=None):
|
|
||||||
if not isinstance(interval, timedelta):
|
|
||||||
raise TypeError('interval must be a timedelta')
|
|
||||||
if start_date:
|
|
||||||
start_date = convert_to_datetime(start_date)
|
|
||||||
|
|
||||||
self.interval = interval
|
|
||||||
self.interval_length = timedelta_seconds(self.interval)
|
|
||||||
if self.interval_length == 0:
|
|
||||||
self.interval = timedelta(seconds=1)
|
|
||||||
self.interval_length = 1
|
|
||||||
|
|
||||||
if start_date is None:
|
|
||||||
self.start_date = datetime.now() + self.interval
|
|
||||||
else:
|
|
||||||
self.start_date = convert_to_datetime(start_date)
|
|
||||||
|
|
||||||
def get_next_fire_time(self, start_date):
|
|
||||||
if start_date < self.start_date:
|
|
||||||
return self.start_date
|
|
||||||
|
|
||||||
timediff_seconds = timedelta_seconds(start_date - self.start_date)
|
|
||||||
next_interval_num = int(ceil(timediff_seconds / self.interval_length))
|
|
||||||
return self.start_date + self.interval * next_interval_num
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'interval[%s]' % str(self.interval)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s (interval=%s, start_date=%s)>" % (
|
|
||||||
self.__class__.__name__, repr(self.interval),
|
|
||||||
repr(self.start_date))
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from apscheduler.util import convert_to_datetime
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTrigger(object):
|
|
||||||
def __init__(self, run_date):
|
|
||||||
self.run_date = convert_to_datetime(run_date)
|
|
||||||
|
|
||||||
def get_next_fire_time(self, start_date):
|
|
||||||
if self.run_date >= start_date:
|
|
||||||
return self.run_date
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'date[%s]' % str(self.run_date)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (run_date=%s)>' % (
|
|
||||||
self.__class__.__name__, repr(self.run_date))
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
"""
|
|
||||||
This module contains several handy functions primarily meant for internal use.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import date, datetime, timedelta
|
|
||||||
from time import mktime
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
__all__ = ('asint', 'asbool', 'convert_to_datetime', 'timedelta_seconds',
|
|
||||||
'time_difference', 'datetime_ceil', 'combine_opts',
|
|
||||||
'get_callable_name', 'obj_to_ref', 'ref_to_obj', 'maybe_ref',
|
|
||||||
'to_unicode', 'iteritems', 'itervalues', 'xrange')
|
|
||||||
|
|
||||||
|
|
||||||
def asint(text):
|
|
||||||
"""
|
|
||||||
Safely converts a string to an integer, returning None if the string
|
|
||||||
is None.
|
|
||||||
|
|
||||||
:type text: str
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
if text is not None:
|
|
||||||
return int(text)
|
|
||||||
|
|
||||||
|
|
||||||
def asbool(obj):
|
|
||||||
"""
|
|
||||||
Interprets an object as a boolean value.
|
|
||||||
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
if isinstance(obj, str):
|
|
||||||
obj = obj.strip().lower()
|
|
||||||
if obj in ('true', 'yes', 'on', 'y', 't', '1'):
|
|
||||||
return True
|
|
||||||
if obj in ('false', 'no', 'off', 'n', 'f', '0'):
|
|
||||||
return False
|
|
||||||
raise ValueError('Unable to interpret value "%s" as boolean' % obj)
|
|
||||||
return bool(obj)
|
|
||||||
|
|
||||||
|
|
||||||
_DATE_REGEX = re.compile(
|
|
||||||
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
|
|
||||||
r'(?: (?P<hour>\d{1,2}):(?P<minute>\d{1,2}):(?P<second>\d{1,2})'
|
|
||||||
r'(?:\.(?P<microsecond>\d{1,6}))?)?')
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_datetime(input):
|
|
||||||
"""
|
|
||||||
Converts the given object to a datetime object, if possible.
|
|
||||||
If an actual datetime object is passed, it is returned unmodified.
|
|
||||||
If the input is a string, it is parsed as a datetime.
|
|
||||||
|
|
||||||
Date strings are accepted in three different forms: date only (Y-m-d),
|
|
||||||
date with time (Y-m-d H:M:S) or with date+time with microseconds
|
|
||||||
(Y-m-d H:M:S.micro).
|
|
||||||
|
|
||||||
:rtype: datetime
|
|
||||||
"""
|
|
||||||
if isinstance(input, datetime):
|
|
||||||
return input
|
|
||||||
elif isinstance(input, date):
|
|
||||||
return datetime.fromordinal(input.toordinal())
|
|
||||||
elif isinstance(input, str):
|
|
||||||
m = _DATE_REGEX.match(input)
|
|
||||||
if not m:
|
|
||||||
raise ValueError('Invalid date string')
|
|
||||||
values = [(k, int(v or 0)) for k, v in m.groupdict().items()]
|
|
||||||
values = dict(values)
|
|
||||||
return datetime(**values)
|
|
||||||
raise TypeError('Unsupported input type: %s' % type(input))
|
|
||||||
|
|
||||||
|
|
||||||
def timedelta_seconds(delta):
|
|
||||||
"""
|
|
||||||
Converts the given timedelta to seconds.
|
|
||||||
|
|
||||||
:type delta: timedelta
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
return delta.days * 24 * 60 * 60 + delta.seconds + \
|
|
||||||
delta.microseconds / 1000000.0
|
|
||||||
|
|
||||||
|
|
||||||
def time_difference(date1, date2):
|
|
||||||
"""
|
|
||||||
Returns the time difference in seconds between the given two
|
|
||||||
datetime objects. The difference is calculated as: date1 - date2.
|
|
||||||
|
|
||||||
:param date1: the later datetime
|
|
||||||
:type date1: datetime
|
|
||||||
:param date2: the earlier datetime
|
|
||||||
:type date2: datetime
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
later = mktime(date1.timetuple()) + date1.microsecond / 1000000.0
|
|
||||||
earlier = mktime(date2.timetuple()) + date2.microsecond / 1000000.0
|
|
||||||
return later - earlier
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_ceil(dateval):
|
|
||||||
"""
|
|
||||||
Rounds the given datetime object upwards.
|
|
||||||
|
|
||||||
:type dateval: datetime
|
|
||||||
"""
|
|
||||||
if dateval.microsecond > 0:
|
|
||||||
return dateval + timedelta(seconds=1,
|
|
||||||
microseconds=-dateval.microsecond)
|
|
||||||
return dateval
|
|
||||||
|
|
||||||
|
|
||||||
def combine_opts(global_config, prefix, local_config={}):
|
|
||||||
"""
|
|
||||||
Returns a subdictionary from keys and values of ``global_config`` where
|
|
||||||
the key starts with the given prefix, combined with options from
|
|
||||||
local_config. The keys in the subdictionary have the prefix removed.
|
|
||||||
|
|
||||||
:type global_config: dict
|
|
||||||
:type prefix: str
|
|
||||||
:type local_config: dict
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
prefixlen = len(prefix)
|
|
||||||
subconf = {}
|
|
||||||
for key, value in global_config.items():
|
|
||||||
if key.startswith(prefix):
|
|
||||||
key = key[prefixlen:]
|
|
||||||
subconf[key] = value
|
|
||||||
subconf.update(local_config)
|
|
||||||
return subconf
|
|
||||||
|
|
||||||
|
|
||||||
def get_callable_name(func):
|
|
||||||
"""
|
|
||||||
Returns the best available display name for the given function/callable.
|
|
||||||
"""
|
|
||||||
name = func.__module__
|
|
||||||
if hasattr(func, '__self__') and func.__self__:
|
|
||||||
name += '.' + func.__self__.__name__
|
|
||||||
elif hasattr(func, 'im_self') and func.im_self: # py2.4, 2.5
|
|
||||||
name += '.' + func.im_self.__name__
|
|
||||||
if hasattr(func, '__name__'):
|
|
||||||
name += '.' + func.__name__
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def obj_to_ref(obj):
|
|
||||||
"""
|
|
||||||
Returns the path to the given object.
|
|
||||||
"""
|
|
||||||
ref = '%s:%s' % (obj.__module__, obj.__name__)
|
|
||||||
try:
|
|
||||||
obj2 = ref_to_obj(ref)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if obj2 == obj:
|
|
||||||
return ref
|
|
||||||
|
|
||||||
raise ValueError('Only module level objects are supported')
|
|
||||||
|
|
||||||
|
|
||||||
def ref_to_obj(ref):
|
|
||||||
"""
|
|
||||||
Returns the object pointed to by ``ref``.
|
|
||||||
"""
|
|
||||||
modulename, rest = ref.split(':', 1)
|
|
||||||
obj = __import__(modulename)
|
|
||||||
for name in modulename.split('.')[1:] + rest.split('.'):
|
|
||||||
obj = getattr(obj, name)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_ref(ref):
|
|
||||||
"""
|
|
||||||
Returns the object that the given reference points to, if it is indeed
|
|
||||||
a reference. If it is not a reference, the object is returned as-is.
|
|
||||||
"""
|
|
||||||
if not isinstance(ref, str):
|
|
||||||
return ref
|
|
||||||
return ref_to_obj(ref)
|
|
||||||
|
|
||||||
|
|
||||||
def to_unicode(string, encoding='ascii'):
|
|
||||||
"""
|
|
||||||
Safely converts a string to a unicode representation on any
|
|
||||||
Python version.
|
|
||||||
"""
|
|
||||||
if hasattr(string, 'decode'):
|
|
||||||
return string.decode(encoding, 'ignore')
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 0): # pragma: nocover
|
|
||||||
iteritems = lambda d: d.iteritems()
|
|
||||||
itervalues = lambda d: d.itervalues()
|
|
||||||
xrange = xrange
|
|
||||||
else: # pragma: nocover
|
|
||||||
iteritems = lambda d: d.items()
|
|
||||||
itervalues = lambda d: d.values()
|
|
||||||
xrange = range
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import os
|
|
||||||
from configobj import ConfigObj
|
|
||||||
from headphones import config_file
|
|
||||||
|
|
||||||
config = ConfigObj(config_file)
|
|
||||||
|
|
||||||
General = config['General']
|
|
||||||
http_host = General['http_host']
|
|
||||||
http_port = General['http_port']
|
|
||||||
http_username = General['http_username']
|
|
||||||
http_password = General['http_password']
|
|
||||||
try:
|
|
||||||
http_root = General['http_root']
|
|
||||||
except KeyError:
|
|
||||||
General['http_root'] = ''
|
|
||||||
config.write()
|
|
||||||
launch_browser = General['launch_browser']
|
|
||||||
usenet_retention = General['usenet_retention']
|
|
||||||
include_lossless = General['include_lossless']
|
|
||||||
flac_to_mp3 = General['flac_to_mp3']
|
|
||||||
move_to_itunes = General['move_to_itunes']
|
|
||||||
path_to_itunes = General['path_to_itunes']
|
|
||||||
rename_mp3s = General['rename_mp3s']
|
|
||||||
cleanup = General['cleanup']
|
|
||||||
add_album_art = General['add_album_art']
|
|
||||||
music_download_dir = General['music_download_dir']
|
|
||||||
NZBMatrix = config['NZBMatrix']
|
|
||||||
nzbmatrix = NZBMatrix['nzbmatrix']
|
|
||||||
nzbmatrix_username = NZBMatrix['nzbmatrix_username']
|
|
||||||
nzbmatrix_apikey = NZBMatrix['nzbmatrix_apikey']
|
|
||||||
Newznab = config['Newznab']
|
|
||||||
newznab = Newznab['newznab']
|
|
||||||
newznab_host = Newznab['newznab_host']
|
|
||||||
newznab_apikey = Newznab['newznab_apikey']
|
|
||||||
NZBsorg = config['NZBsorg']
|
|
||||||
nzbsorg = NZBsorg['nzbsorg']
|
|
||||||
nzbsorg_uid = NZBsorg['nzbsorg_uid']
|
|
||||||
nzbsorg_hash = NZBsorg['nzbsorg_hash']
|
|
||||||
SABnzbd = config['SABnzbd']
|
|
||||||
sab_username = SABnzbd['sab_username']
|
|
||||||
sab_password = SABnzbd['sab_password']
|
|
||||||
sab_apikey = SABnzbd['sab_apikey']
|
|
||||||
sab_category = SABnzbd['sab_category']
|
|
||||||
sab_host = SABnzbd['sab_host']
|
|
||||||
|
|
||||||
def var_to_chk(variable):
|
|
||||||
if variable == '1':
|
|
||||||
return 'Checked'
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
form = '''
|
|
||||||
<br>
|
|
||||||
<center>
|
|
||||||
<div class="smalltext">
|
|
||||||
<a href="#web_interface" >Web Interface</a> |
|
|
||||||
<a href="#download" class="smalltext">Download Settings</a> |
|
|
||||||
<a href="#providers" class="smalltext">Search Providers</a> |
|
|
||||||
<a href="#post_processing" class="smalltext">Quality & Post Processing</a>
|
|
||||||
</div>
|
|
||||||
</center>
|
|
||||||
<div class="table">
|
|
||||||
<div class="config">
|
|
||||||
<form action="configUpdate" method="post">
|
|
||||||
<a name="web_interface"><h1><u>Web Interface</u></h1></a>
|
|
||||||
|
|
||||||
<table class="configtable" summary="Web Interface">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
HTTP Host: <br><br>
|
|
||||||
<input type="text" name="http_host" value="%s" size="30" maxlength="40"><br>
|
|
||||||
<i class="smalltext">i.e. localhost or 0.0.0.0</i>
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
HTTP Username: <br><br>
|
|
||||||
<input type="text" name="http_username" value="%s" size="30" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
HTTP Port: <br><br>
|
|
||||||
<input type="text" name="http_port" value="%s" size="20" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
HTTP Password: <br><br>
|
|
||||||
<input type="password" name="http_password" value="%s" size="30" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p>Launch Browser on Startup:<input type="checkbox" name="launch_browser" value="1" %s /></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<a name="download"><h1><u>Download Settings</u></h1></a>
|
|
||||||
|
|
||||||
<table class="configtable" summary="Download Settings">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p>SABnzbd Host:</p><input type="text" name="sab_host" value="%s" size="30" maxlength="40"><br>
|
|
||||||
|
|
||||||
<i class="smalltext">usually localhost:8080</i>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<p>SABnzbd Username:</p><input type="text" name="sab_username" value="%s" size="20" maxlength="40">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>SABnzbd API:</p><input type="text" name="sab_apikey" value="%s" size="46" maxlength="40">
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>SABnzbd Password:</p><input type="password" name="sab_password" value="%s" size="20" maxlength="40">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>SABnzbd Category:</p><input type="text" name="sab_category" value="%s" size="20" maxlength="40">
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>Music Download Directory:</p><input type="text" name="music_download_dir" value="%s" size="60" maxlength="40"><br>
|
|
||||||
|
|
||||||
<i class="smalltext">Absolute or relative path to the dir where SAB downloads your music<br>
|
|
||||||
i.e. Downloads/music or /Users/name/Downloads/music</i>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>Usenet Retention:</p><input type="text" name="usenet_retention" value="%s" size="20" maxlength="40">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<a name="providers"><h1><u>Search Providers</u></h1></a>
|
|
||||||
|
|
||||||
<table class="configtable" summary="Search Providers">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p>NZBMatrix: <input type="checkbox" name="nzbmatrix" value="1" %s /></p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
NZBMatrix Username: <br>
|
|
||||||
<input type="text" name="nzbmatrix_username" value="%s" size="30" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
NZBMatrix API: <br>
|
|
||||||
<input type="text" name="nzbmatrix_apikey" value="%s" size="46" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>Newznab: <input type="checkbox" name="newznab" value="1" %s /></p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Newznab Host:<br>
|
|
||||||
<input type="text" name="newznab_host" value="%s" size="30" maxlength="40"><br>
|
|
||||||
<i class="smalltext">i.e. http://nzb.su</i>
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Newznab API:<br>
|
|
||||||
<input type="text" name="newznab_apikey" value="%s" size="46" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>NZBs.org:<input type="checkbox" name="nzbsorg" value="1" %s /></p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
NZBs.org UID:<br>
|
|
||||||
<input type="text" name="nzbsorg_uid" value="%s" size="30" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
NZBs.org Hash:<br>
|
|
||||||
<input type="text" name="nzbsorg_hash" value="%s" size="46" maxlength="40">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<a name="post_processing"><h1><u>Quality & Post Processing</u></h1></a>
|
|
||||||
|
|
||||||
<table class="configtable" summary="Quality & Post Processing">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p><b>Album Quality:</b></p>
|
|
||||||
<input type="checkbox" name="include_lossless" value="1" %s />Include lossless <br>
|
|
||||||
<input type="checkbox" name="flac_to_mp3" value="1" %s />Convert lossless to mp3
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
<p><b>iTunes:</b></p>
|
|
||||||
<input type="checkbox" name="move_to_itunes" value="1" %s />Move downloads to iTunes
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p><b>Path to Music folder</b>:<br><input type="text" name="path_to_itunes" value="%s" size="60" maxlength="40">
|
|
||||||
<br>
|
|
||||||
<i class="smalltext">i.e. /Users/name/Music/iTunes or /Volumes/share/music</i>
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b>Renaming & Metadata:</b>
|
|
||||||
<p>
|
|
||||||
<input type="checkbox" name="rename_mp3s" value="1" %s />Rename & add metadata
|
|
||||||
<br>
|
|
||||||
<input type="checkbox" name="cleanup" value="1" %s />Delete leftover files
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<br>
|
|
||||||
<p><b>Album Art:</b></p>
|
|
||||||
<input type="checkbox" name="add_album_art" value="1" %s>Add album art
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p class="center"><input type="submit" value="Save Changes"><br>
|
|
||||||
(For now, all changes require a restart to take effect)</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>''' % (http_host, http_username, http_port, http_password, var_to_chk(launch_browser), sab_host, sab_username, sab_apikey, sab_password, sab_category, music_download_dir, usenet_retention, var_to_chk(nzbmatrix), nzbmatrix_username, nzbmatrix_apikey, var_to_chk(newznab), newznab_host, newznab_apikey, var_to_chk(nzbsorg), nzbsorg_uid, nzbsorg_hash, var_to_chk(include_lossless), var_to_chk(flac_to_mp3), var_to_chk(move_to_itunes), path_to_itunes, var_to_chk(rename_mp3s), var_to_chk(cleanup), var_to_chk(add_album_art))
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from configobj import ConfigObj
|
|
||||||
|
|
||||||
def configCreate(path):
|
|
||||||
config = ConfigObj()
|
|
||||||
config.filename = path
|
|
||||||
config['General'] = {}
|
|
||||||
config['General']['http_host'] = '0.0.0.0'
|
|
||||||
config['General']['http_port'] = 8181
|
|
||||||
config['General']['http_username'] = ''
|
|
||||||
config['General']['http_password'] = ''
|
|
||||||
config['General']['http_root'] = ''
|
|
||||||
config['General']['launch_browser'] = 1
|
|
||||||
config['General']['include_lossless'] = 0
|
|
||||||
config['General']['flac_to_mp3'] = 0
|
|
||||||
config['General']['move_to_itunes'] = 0
|
|
||||||
config['General']['path_to_itunes'] = ''
|
|
||||||
config['General']['rename_mp3s'] = 0
|
|
||||||
config['General']['cleanup'] = 0
|
|
||||||
config['General']['add_album_art'] = 0
|
|
||||||
config['General']['music_download_dir'] = ''
|
|
||||||
config['General']['usenet_retention'] = 500
|
|
||||||
config['SABnzbd'] = {}
|
|
||||||
config['SABnzbd']['sab_host'] = ''
|
|
||||||
config['SABnzbd']['sab_username'] = ''
|
|
||||||
config['SABnzbd']['sab_password'] = ''
|
|
||||||
config['SABnzbd']['sab_apikey'] = ''
|
|
||||||
config['SABnzbd']['sab_category'] = ''
|
|
||||||
config['NZBMatrix'] = {}
|
|
||||||
config['NZBMatrix']['nzbmatrix'] = 0
|
|
||||||
config['NZBMatrix']['nzbmatrix_username'] = ''
|
|
||||||
config['NZBMatrix']['nzbmatrix_apikey'] = ''
|
|
||||||
config['Newznab'] = {}
|
|
||||||
config['Newznab']['newznab'] = 0
|
|
||||||
config['Newznab']['newznab_host'] = ''
|
|
||||||
config['Newznab']['newznab_apikey'] = ''
|
|
||||||
config['NZBsorg'] = {}
|
|
||||||
config['NZBsorg']['nzbsorg'] = 0
|
|
||||||
config['NZBsorg']['nzbsorg_uid'] = ''
|
|
||||||
config['NZBsorg']['nzbsorg_hash'] = ''
|
|
||||||
|
|
||||||
config.write()
|
|
||||||
-2468
File diff suppressed because it is too large
Load Diff
+17
-1
@@ -89,6 +89,9 @@ h1{
|
|||||||
.bigtext{
|
.bigtext{
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
.updatebar{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
a:link {
|
a:link {
|
||||||
color: #5E2612;
|
color: #5E2612;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -121,4 +124,17 @@ a.green {
|
|||||||
a.externalred {
|
a.externalred {
|
||||||
color: red;
|
color: red;
|
||||||
font-size:12px;
|
font-size:12px;
|
||||||
}
|
}
|
||||||
|
div.progress-container {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: 100px;
|
||||||
|
margin: 2px 5px 2px 0;
|
||||||
|
padding: 1px;
|
||||||
|
float: left;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.progress-container > div {
|
||||||
|
background-color: #ACE97C;
|
||||||
|
height: 12px
|
||||||
|
}
|
||||||
-3909
File diff suppressed because it is too large
Load Diff
+91
-121
@@ -1,135 +1,105 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import cherrypy
|
import os, sys
|
||||||
from cherrypy.process.plugins import Daemonizer
|
|
||||||
from optparse import OptionParser
|
from lib.configobj import ConfigObj
|
||||||
from configobj import ConfigObj
|
|
||||||
from configcreate import configCreate
|
import headphones
|
||||||
import webbrowser
|
|
||||||
import sqlite3
|
from headphones import webstart, logger
|
||||||
import webServer
|
|
||||||
import logger
|
try:
|
||||||
import time
|
import argparse
|
||||||
from threadtools import threadtool
|
except ImportError:
|
||||||
import os
|
import lib.argparse as argparse
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
#set up paths
|
# Fixed paths to Headphones
|
||||||
|
if hasattr(sys, 'frozen'):
|
||||||
FULL_PATH = os.path.dirname(os.path.abspath(__file__))
|
headphones.FULL_PATH = os.path.abspath(sys.executable)
|
||||||
config_file = os.path.join(FULL_PATH, 'config.ini')
|
else:
|
||||||
LOG_DIR = os.path.join(FULL_PATH, 'logs')
|
headphones.FULL_PATH = os.path.abspath(__file__)
|
||||||
|
|
||||||
web_root = None
|
|
||||||
|
|
||||||
if os.path.exists(config_file):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
configCreate(config_file)
|
|
||||||
|
|
||||||
settings = ConfigObj(config_file)['General']
|
|
||||||
|
|
||||||
if not os.access(LOG_DIR, os.F_OK):
|
|
||||||
try:
|
|
||||||
os.makedirs(LOG_DIR, 0744)
|
|
||||||
except:
|
|
||||||
print 'Unable to create log dir, logging to screen only'
|
|
||||||
|
|
||||||
def initialize():
|
|
||||||
|
|
||||||
database = os.path.join(FULL_PATH, 'headphones.db')
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT)')
|
|
||||||
c.execute('CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT)')
|
|
||||||
c.execute('CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT)')
|
|
||||||
c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT)')
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
|
|
||||||
def serverstart():
|
|
||||||
|
|
||||||
parser = OptionParser()
|
|
||||||
parser.add_option("-d", "--daemonize", action="store_true", dest="daemonize")
|
|
||||||
parser.add_option("-q", "--quiet", action="store_true", dest="quiet")
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
headphones.PROG_DIR = os.path.dirname(headphones.FULL_PATH)
|
||||||
|
headphones.ARGS = sys.argv[1:]
|
||||||
consoleLogging=True
|
|
||||||
|
|
||||||
if options.quiet or options.daemonize:
|
# Set up and gather command line arguments
|
||||||
cherrypy.config.update({'log.screen': False})
|
parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+')
|
||||||
consoleLogging=False
|
|
||||||
|
|
||||||
cherrypy.config.update({
|
parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging')
|
||||||
'server.thread_pool': 10,
|
parser.add_argument('-d', '--daemon', action='store_true', help='Run as a daemon')
|
||||||
'server.socket_port': int(settings['http_port']),
|
parser.add_argument('-p', '--port', type=int, help='Force Headphones to run on a specified port')
|
||||||
'server.socket_host': settings['http_host']
|
parser.add_argument('--datadir', help='Specify a directory where to store your data files')
|
||||||
})
|
parser.add_argument('--config', help='Specify a config file to use')
|
||||||
|
parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup')
|
||||||
conf = {
|
|
||||||
'/': {
|
args = parser.parse_args()
|
||||||
'tools.staticdir.root': FULL_PATH
|
|
||||||
},
|
if args.quiet:
|
||||||
'/data/images':{
|
headphones.QUIET=True
|
||||||
'tools.staticdir.on': True,
|
|
||||||
'tools.staticdir.dir': "data/images"
|
if args.daemon:
|
||||||
},
|
headphones.DAEMON=True
|
||||||
'/data/css':{
|
headphones.QUIET=True
|
||||||
'tools.staticdir.on': True,
|
|
||||||
'tools.staticdir.dir': "data/css"
|
|
||||||
},
|
|
||||||
'/data/js':{
|
|
||||||
'tools.staticdir.on': True,
|
|
||||||
'tools.staticdir.dir': "data/js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if settings['http_password'] != "":
|
|
||||||
conf['/'].update({
|
|
||||||
'tools.auth_basic.on': True,
|
|
||||||
'tools.auth_basic.realm': 'mordor',
|
|
||||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict(
|
|
||||||
{settings['http_username']:settings['http_password']})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if args.datadir:
|
||||||
if options.daemonize:
|
headphones.DATA_DIR = args.datadir
|
||||||
Daemonizer(cherrypy.engine).subscribe()
|
else:
|
||||||
|
headphones.DATA_DIR = headphones.PROG_DIR
|
||||||
|
|
||||||
#Start threads
|
if args.config:
|
||||||
threadtool(cherrypy.engine).subscribe()
|
headphones.CONFIG_FILE = args.config
|
||||||
cherrypy.engine.timeout_monitor.unsubscribe()
|
else:
|
||||||
|
headphones.CONFIG_FILE = os.path.join(headphones.DATA_DIR, 'config.ini')
|
||||||
logger.sb_log_instance.initLogging(consoleLogging=consoleLogging)
|
|
||||||
|
|
||||||
global web_root
|
|
||||||
try:
|
|
||||||
web_root = settings['http_root']
|
|
||||||
except KeyError:
|
|
||||||
web_root = '/'
|
|
||||||
|
|
||||||
def browser():
|
|
||||||
if settings['http_host'] == '0.0.0.0':
|
|
||||||
host = 'localhost'
|
|
||||||
else:
|
|
||||||
host = settings['http_host']
|
|
||||||
webbrowser.open('http://' + host + ':' + settings['http_port'] + web_root)
|
|
||||||
|
|
||||||
|
# Try to create the DATA_DIR if it doesn't exist
|
||||||
|
if not os.path.exists(headphones.DATA_DIR):
|
||||||
|
try:
|
||||||
|
os.makedirs(headphones.DATA_DIR)
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit('Could not create data directory: ' + headphones.DATA_DIR + '. Exiting....')
|
||||||
|
|
||||||
if settings['launch_browser'] == '1':
|
# Make sure the DATA_DIR is writeable
|
||||||
cherrypy.engine.subscribe('start', browser, priority=90)
|
if not os.access(headphones.DATA_DIR, os.W_OK):
|
||||||
|
raise SystemExit('Cannot write to the data directory: ' + headphones.DATA_DIR + '. Exiting...')
|
||||||
|
|
||||||
logger.log(u"Starting Headphones on port:" + settings['http_port'])
|
# Put the database in the DATA_DIR
|
||||||
|
headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db')
|
||||||
|
|
||||||
|
headphones.CFG = ConfigObj(headphones.CONFIG_FILE)
|
||||||
|
|
||||||
|
# Read config & start logging
|
||||||
|
headphones.initialize()
|
||||||
|
|
||||||
|
if headphones.DAEMON:
|
||||||
|
headphones.daemonize()
|
||||||
|
|
||||||
|
# Force the http port if neccessary
|
||||||
|
if args.port:
|
||||||
|
http_port = args.port
|
||||||
|
logger.info('Starting Headphones on foced port: %i' % http_port)
|
||||||
|
else:
|
||||||
|
http_port = int(headphones.HTTP_PORT)
|
||||||
|
|
||||||
|
# Try to start the server.
|
||||||
|
webstart.initialize({
|
||||||
|
'http_port': http_port,
|
||||||
|
'http_host': headphones.HTTP_HOST,
|
||||||
|
'http_root': headphones.HTTP_ROOT,
|
||||||
|
'http_username': headphones.HTTP_USERNAME,
|
||||||
|
'http_password': headphones.HTTP_PASSWORD,
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('Starting Headphones on port: %i' % http_port)
|
||||||
|
|
||||||
|
if headphones.LAUNCH_BROWSER and not args.nolaunch:
|
||||||
|
headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT)
|
||||||
|
|
||||||
|
# Start the background threads
|
||||||
|
headphones.start()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
cherrypy.quickstart(webServer.Headphones(), web_root, config = conf)
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
initialize()
|
|
||||||
serverstart()
|
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
def multikeysort(items, columns):
|
|
||||||
from operator import itemgetter
|
|
||||||
comparers = [ ((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
|
||||||
def comparer(left, right):
|
|
||||||
for fn, mult in comparers:
|
|
||||||
result = cmp(fn(left), fn(right))
|
|
||||||
if result:
|
|
||||||
return mult * result
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
return sorted(items, cmp=comparer)
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
from pyItunes import *
|
|
||||||
from configobj import ConfigObj
|
|
||||||
import musicbrainz2.webservice as ws
|
|
||||||
import musicbrainz2.model as m
|
|
||||||
import musicbrainz2.utils as u
|
|
||||||
from mb import getReleaseGroup
|
|
||||||
import string
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
from headphones import FULL_PATH
|
|
||||||
from lib.beets.mediafile import MediaFile
|
|
||||||
|
|
||||||
import logger
|
|
||||||
|
|
||||||
database = os.path.join(FULL_PATH, 'headphones.db')
|
|
||||||
|
|
||||||
|
|
||||||
def scanMusic(dir):
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for r,d,f in os.walk(dir):
|
|
||||||
for files in f:
|
|
||||||
if any(files.endswith(x) for x in (".mp3", ".flac", ".aac", ".ogg", ".ape")):
|
|
||||||
results.append(os.path.join(r,files))
|
|
||||||
|
|
||||||
logger.log(u'%i music files found' % len(results))
|
|
||||||
|
|
||||||
lst = []
|
|
||||||
|
|
||||||
for song in results:
|
|
||||||
try:
|
|
||||||
f = MediaFile(song)
|
|
||||||
except:
|
|
||||||
logger.log("Could not read file: '" + song + "'", logger.ERROR)
|
|
||||||
else:
|
|
||||||
if not f.artist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
lst.append(f.artist)
|
|
||||||
|
|
||||||
artistlist = {}.fromkeys(lst).keys()
|
|
||||||
logger.log(u"Preparing to import %i artists" % len(artistlist))
|
|
||||||
importartist(artistlist)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def itunesImport(pathtoxml):
|
|
||||||
if os.path.splitext(pathtoxml)[1] == '.xml':
|
|
||||||
logger.log(u"Loading xml file from"+ pathtoxml)
|
|
||||||
pl = XMLLibraryParser(pathtoxml)
|
|
||||||
l = Library(pl.dictionary)
|
|
||||||
lst = []
|
|
||||||
for song in l.songs:
|
|
||||||
lst.append(song.artist)
|
|
||||||
rawlist = {}.fromkeys(lst).keys()
|
|
||||||
artistlist = [f for f in rawlist if f != None]
|
|
||||||
importartist(artistlist)
|
|
||||||
else:
|
|
||||||
rawlist = os.listdir(pathtoxml)
|
|
||||||
logger.log(u"Loading artists from directory:" +pathtoxml)
|
|
||||||
exclude = ['.ds_store', 'various artists', 'untitled folder', 'va']
|
|
||||||
artistlist = [f for f in rawlist if f.lower() not in exclude]
|
|
||||||
importartist(artistlist)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def importartist(artistlist):
|
|
||||||
for name in artistlist:
|
|
||||||
logger.log(u"Querying MusicBrainz for: "+name)
|
|
||||||
artistResults = ws.Query().getArtists(ws.ArtistFilter(string.replace(name, '&', '%38'), limit=1))
|
|
||||||
for result in artistResults:
|
|
||||||
if result.artist.name == 'Various Artists':
|
|
||||||
logger.log(u"Top result is Various Artists. Skipping.", logger.WARNING)
|
|
||||||
else:
|
|
||||||
logger.log(u"Found best match: "+result.artist.name+". Gathering album information...")
|
|
||||||
time.sleep(1)
|
|
||||||
artistid = u.extractUuid(result.artist.id)
|
|
||||||
inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), releaseGroups=True)
|
|
||||||
artist = ws.Query().getArtistById(artistid, inc)
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('SELECT ArtistID from artists')
|
|
||||||
artistlist = c.fetchall()
|
|
||||||
if any(artistid in x for x in artistlist):
|
|
||||||
logger.log(result.artist.name + u" is already in the database, skipping")
|
|
||||||
else:
|
|
||||||
c.execute('INSERT INTO artists VALUES( ?, ?, ?, CURRENT_DATE, ?)', (artistid, artist.name, artist.sortName, 'Active'))
|
|
||||||
for rg in artist.getReleaseGroups():
|
|
||||||
rgid = u.extractUuid(rg.id)
|
|
||||||
|
|
||||||
releaseid = getReleaseGroup(rgid)
|
|
||||||
|
|
||||||
inc = ws.ReleaseIncludes(artist=True, releaseEvents= True, tracks= True, releaseGroup=True)
|
|
||||||
results = ws.Query().getReleaseById(releaseid, inc)
|
|
||||||
|
|
||||||
logger.log(u"Now adding album: " + results.title+ " to the database")
|
|
||||||
c.execute('INSERT INTO albums VALUES( ?, ?, ?, ?, ?, CURRENT_DATE, ?, ?)', (artistid, results.artist.name, results.title, results.asin, results.getEarliestReleaseDate(), u.extractUuid(results.id), 'Skipped'))
|
|
||||||
conn.commit()
|
|
||||||
c.execute('SELECT ReleaseDate, DateAdded from albums WHERE AlbumID="%s"' % u.extractUuid(results.id))
|
|
||||||
|
|
||||||
latestrelease = c.fetchall()
|
|
||||||
|
|
||||||
if latestrelease[0][0] > latestrelease[0][1]:
|
|
||||||
logger.log(results.title + u" is an upcoming album. Setting its status to 'Wanted'...")
|
|
||||||
c.execute('UPDATE albums SET Status = "Wanted" WHERE AlbumID="%s"' % u.extractUuid(results.id))
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for track in results.tracks:
|
|
||||||
c.execute('INSERT INTO tracks VALUES( ?, ?, ?, ?, ?, ?, ?, ?)', (artistid, results.artist.name, results.title, results.asin, u.extractUuid(results.id), track.title, track.duration, u.extractUuid(track.id)))
|
|
||||||
time.sleep(1)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
# Author: Nic Wolfe <nic@wolfeden.ca>
|
|
||||||
# URL: http://code.google.com/p/sickbeard/
|
|
||||||
#
|
|
||||||
# This file is part of Sick Beard.
|
|
||||||
#
|
|
||||||
# Sick Beard is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Sick Beard is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import headphones
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
# number of log files to keep
|
|
||||||
NUM_LOGS = 3
|
|
||||||
|
|
||||||
# log size in bytes
|
|
||||||
LOG_SIZE = 10000000 # 10 megs
|
|
||||||
|
|
||||||
ERROR = logging.ERROR
|
|
||||||
WARNING = logging.WARNING
|
|
||||||
MESSAGE = logging.INFO
|
|
||||||
DEBUG = logging.DEBUG
|
|
||||||
|
|
||||||
reverseNames = {u'ERROR': ERROR,
|
|
||||||
u'WARNING': WARNING,
|
|
||||||
u'INFO': MESSAGE,
|
|
||||||
u'DEBUG': DEBUG}
|
|
||||||
|
|
||||||
class SBRotatingLogHandler(object):
|
|
||||||
|
|
||||||
def __init__(self, log_file, num_files, num_bytes):
|
|
||||||
self.num_files = num_files
|
|
||||||
self.num_bytes = num_bytes
|
|
||||||
|
|
||||||
self.log_file = log_file
|
|
||||||
self.cur_handler = None
|
|
||||||
|
|
||||||
self.writes_since_check = 0
|
|
||||||
|
|
||||||
self.log_lock = threading.Lock()
|
|
||||||
|
|
||||||
def initLogging(self, consoleLogging=True):
|
|
||||||
|
|
||||||
self.log_file = os.path.join(headphones.LOG_DIR, self.log_file)
|
|
||||||
|
|
||||||
self.cur_handler = self._config_handler()
|
|
||||||
|
|
||||||
logging.getLogger('headphones').addHandler(self.cur_handler)
|
|
||||||
|
|
||||||
# define a Handler which writes INFO messages or higher to the sys.stderr
|
|
||||||
if consoleLogging:
|
|
||||||
console = logging.StreamHandler()
|
|
||||||
|
|
||||||
console.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# set a format which is simpler for console use
|
|
||||||
console.setFormatter(logging.Formatter('%(asctime)s %(levelname)s::%(message)s', '%H:%M:%S'))
|
|
||||||
|
|
||||||
# add the handler to the root logger
|
|
||||||
logging.getLogger('headphones').addHandler(console)
|
|
||||||
|
|
||||||
logging.getLogger('headphones').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
def _config_handler(self):
|
|
||||||
"""
|
|
||||||
Configure a file handler to log at file_name and return it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
file_handler = logging.FileHandler(self.log_file)
|
|
||||||
file_handler.setLevel(logging.DEBUG)
|
|
||||||
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', '%b-%d %H:%M:%S'))
|
|
||||||
return file_handler
|
|
||||||
|
|
||||||
def _log_file_name(self, i):
|
|
||||||
"""
|
|
||||||
Returns a numbered log file name depending on i. If i==0 it just uses logName, if not it appends
|
|
||||||
it to the extension (blah.log.3 for i == 3)
|
|
||||||
|
|
||||||
i: Log number to ues
|
|
||||||
"""
|
|
||||||
return self.log_file + ('.' + str(i) if i else '')
|
|
||||||
|
|
||||||
def _num_logs(self):
|
|
||||||
"""
|
|
||||||
Scans the log folder and figures out how many log files there are already on disk
|
|
||||||
|
|
||||||
Returns: The number of the last used file (eg. mylog.log.3 would return 3). If there are no logs it returns -1
|
|
||||||
"""
|
|
||||||
cur_log = 0
|
|
||||||
while os.path.isfile(self._log_file_name(cur_log)):
|
|
||||||
cur_log += 1
|
|
||||||
return cur_log - 1
|
|
||||||
|
|
||||||
def _rotate_logs(self):
|
|
||||||
|
|
||||||
sb_logger = logging.getLogger('headphones')
|
|
||||||
|
|
||||||
# delete the old handler
|
|
||||||
if self.cur_handler:
|
|
||||||
self.cur_handler.flush()
|
|
||||||
self.cur_handler.close()
|
|
||||||
sb_logger.removeHandler(self.cur_handler)
|
|
||||||
|
|
||||||
# rename or delete all the old log files
|
|
||||||
for i in range(self._num_logs(), -1, -1):
|
|
||||||
cur_file_name = self._log_file_name(i)
|
|
||||||
try:
|
|
||||||
if i >= NUM_LOGS:
|
|
||||||
os.remove(cur_file_name)
|
|
||||||
else:
|
|
||||||
os.rename(cur_file_name, self._log_file_name(i+1))
|
|
||||||
except WindowsError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# the new log handler will always be on the un-numbered .log file
|
|
||||||
new_file_handler = self._config_handler()
|
|
||||||
|
|
||||||
self.cur_handler = new_file_handler
|
|
||||||
|
|
||||||
sb_logger.addHandler(new_file_handler)
|
|
||||||
|
|
||||||
def log(self, toLog, logLevel=MESSAGE):
|
|
||||||
|
|
||||||
with self.log_lock:
|
|
||||||
|
|
||||||
# check the size and see if we need to rotate
|
|
||||||
if self.writes_since_check >= 10:
|
|
||||||
if os.path.isfile(self.log_file) and os.path.getsize(self.log_file) >= LOG_SIZE:
|
|
||||||
self._rotate_logs()
|
|
||||||
self.writes_since_check = 0
|
|
||||||
else:
|
|
||||||
self.writes_since_check += 1
|
|
||||||
|
|
||||||
meThread = threading.currentThread().getName()
|
|
||||||
message = meThread + u" :: " + toLog
|
|
||||||
|
|
||||||
out_line = message.encode('utf-8')
|
|
||||||
|
|
||||||
sb_logger = logging.getLogger('headphones')
|
|
||||||
|
|
||||||
try:
|
|
||||||
if logLevel == DEBUG:
|
|
||||||
sb_logger.debug(out_line)
|
|
||||||
elif logLevel == MESSAGE:
|
|
||||||
sb_logger.info(out_line)
|
|
||||||
elif logLevel == WARNING:
|
|
||||||
sb_logger.warning(out_line)
|
|
||||||
elif logLevel == ERROR:
|
|
||||||
sb_logger.error(out_line)
|
|
||||||
|
|
||||||
# add errors to the UI logger
|
|
||||||
#classes.ErrorViewer.add(classes.UIError(message))
|
|
||||||
else:
|
|
||||||
sb_logger.log(logLevel, out_line)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sb_log_instance = SBRotatingLogHandler('headphones.log', NUM_LOGS, LOG_SIZE)
|
|
||||||
|
|
||||||
def log(toLog, logLevel=MESSAGE):
|
|
||||||
sb_log_instance.log(toLog, logLevel)
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
import musicbrainz2.webservice as ws
|
|
||||||
import musicbrainz2.model as m
|
|
||||||
import musicbrainz2.utils as u
|
|
||||||
|
|
||||||
from musicbrainz2.webservice import WebServiceError
|
|
||||||
|
|
||||||
from helpers import multikeysort
|
|
||||||
|
|
||||||
q = ws.Query()
|
|
||||||
|
|
||||||
|
|
||||||
def findArtist(name, limit=1):
|
|
||||||
|
|
||||||
artistlist = []
|
|
||||||
|
|
||||||
artistResults = q.getArtists(ws.ArtistFilter(name=name, limit=limit))
|
|
||||||
|
|
||||||
for result in artistResults:
|
|
||||||
|
|
||||||
artistid = u.extractUuid(result.artist.id)
|
|
||||||
artistlist.append([result.artist.name, artistid])
|
|
||||||
|
|
||||||
return artistlist
|
|
||||||
|
|
||||||
def getArtist(artistid):
|
|
||||||
|
|
||||||
|
|
||||||
rglist = []
|
|
||||||
|
|
||||||
#Get all official release groups
|
|
||||||
inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), ratings=False, releaseGroups=True)
|
|
||||||
artist = q.getArtistById(artistid, inc)
|
|
||||||
|
|
||||||
for rg in artist.getReleaseGroups():
|
|
||||||
|
|
||||||
rgid = u.extractUuid(rg.id)
|
|
||||||
rglist.append([rg.title, rgid])
|
|
||||||
|
|
||||||
return rglist
|
|
||||||
|
|
||||||
def getReleaseGroup(rgid):
|
|
||||||
|
|
||||||
releaselist = []
|
|
||||||
|
|
||||||
inc = ws.ReleaseGroupIncludes(releases=True)
|
|
||||||
releaseGroup = q.getReleaseGroupById(rgid, inc)
|
|
||||||
|
|
||||||
# I think for now we have to make separate queries for each release, in order
|
|
||||||
# to get more detailed release info (ASIN, track count, etc.)
|
|
||||||
for release in releaseGroup.releases:
|
|
||||||
|
|
||||||
releaseid = u.extractUuid(release.id)
|
|
||||||
inc = ws.ReleaseIncludes(tracks=True)
|
|
||||||
|
|
||||||
releaseResult = q.getReleaseById(releaseid, inc)
|
|
||||||
|
|
||||||
release_dict = {
|
|
||||||
'asin': bool(releaseResult.asin),
|
|
||||||
'tracks': len(releaseResult.getTracks()),
|
|
||||||
'releaseid': u.extractUuid(releaseResult.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
releaselist.append(release_dict)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
a = multikeysort(releaselist, ['-asin', '-tracks'])
|
|
||||||
|
|
||||||
releaseid = a[0]['releaseid']
|
|
||||||
|
|
||||||
return releaseid
|
|
||||||
|
|
||||||
def getRelease(releaseid):
|
|
||||||
"""
|
|
||||||
Given a release id, gather all the info and return it as a list
|
|
||||||
"""
|
|
||||||
inc = ws.ReleaseIncludes(artist=True, tracks=True, releaseGroup=True)
|
|
||||||
release = q.getReleaseById(releaseid, inc)
|
|
||||||
|
|
||||||
releasedetail = []
|
|
||||||
|
|
||||||
releasedetail.append(release.id)
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import glob, os, shutil
|
|
||||||
from configobj import ConfigObj
|
|
||||||
from headphones import config_file
|
|
||||||
|
|
||||||
config = ConfigObj(config_file)
|
|
||||||
|
|
||||||
General = config['General']
|
|
||||||
move_to_itunes = General['move_to_itunes']
|
|
||||||
path_to_itunes = General['path_to_itunes']
|
|
||||||
rename_mp3s = General['rename_mp3s']
|
|
||||||
cleanup = General['cleanup']
|
|
||||||
add_album_art = General['add_album_art']
|
|
||||||
music_download_dir = General['music_download_dir']
|
|
||||||
|
|
||||||
def moveFiles():
|
|
||||||
for root, dirs, files in os.walk(music_download_dir):
|
|
||||||
for file in files:
|
|
||||||
if file[-4:].lower() == '.mp3' and os.path.isfile(file):
|
|
||||||
print file
|
|
||||||
shutil.copy2(os.path.join(root, file),
|
|
||||||
os.path.join(path_to_itunes, file))
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""A collection of classes for MusicBrainz.
|
|
||||||
|
|
||||||
To get started quickly, have a look at L{webservice.Query} and the examples
|
|
||||||
there. The source distribution also contains example code you might find
|
|
||||||
interesting.
|
|
||||||
|
|
||||||
This package contains the following modules:
|
|
||||||
|
|
||||||
1. L{model}: The MusicBrainz domain model, containing classes like
|
|
||||||
L{Artist <model.Artist>}, L{Release <model.Release>}, or
|
|
||||||
L{Track <model.Track>}
|
|
||||||
|
|
||||||
2. L{webservice}: An interface to the MusicBrainz XML web service.
|
|
||||||
|
|
||||||
3. L{wsxml}: A parser for the web service XML format (MMD).
|
|
||||||
|
|
||||||
4. L{disc}: Functions for creating and submitting DiscIDs.
|
|
||||||
|
|
||||||
5. L{utils}: Utilities for working with URIs and other commonly needed tools.
|
|
||||||
|
|
||||||
@author: Matthias Friedrich <matt@mafr.de>
|
|
||||||
"""
|
|
||||||
__revision__ = '$Id: __init__.py 12974 2011-05-01 08:43:54Z luks $'
|
|
||||||
__version__ = '0.7.3'
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
"""Support data for the musicbrainz2 package.
|
|
||||||
|
|
||||||
This package is I{not} part of the public API, it has been added to work
|
|
||||||
around shortcomings in python and may thus be removed at any time.
|
|
||||||
|
|
||||||
Please use the L{musicbrainz2.utils} module instead.
|
|
||||||
"""
|
|
||||||
__revision__ = '$Id: __init__.py 7386 2006-04-30 11:12:55Z matt $'
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__revision__ = '$Id: countrynames.py 7386 2006-04-30 11:12:55Z matt $'
|
|
||||||
|
|
||||||
countryNames = {
|
|
||||||
u'BD': u'Bangladesh',
|
|
||||||
u'BE': u'Belgium',
|
|
||||||
u'BF': u'Burkina Faso',
|
|
||||||
u'BG': u'Bulgaria',
|
|
||||||
u'BB': u'Barbados',
|
|
||||||
u'WF': u'Wallis and Futuna Islands',
|
|
||||||
u'BM': u'Bermuda',
|
|
||||||
u'BN': u'Brunei Darussalam',
|
|
||||||
u'BO': u'Bolivia',
|
|
||||||
u'BH': u'Bahrain',
|
|
||||||
u'BI': u'Burundi',
|
|
||||||
u'BJ': u'Benin',
|
|
||||||
u'BT': u'Bhutan',
|
|
||||||
u'JM': u'Jamaica',
|
|
||||||
u'BV': u'Bouvet Island',
|
|
||||||
u'BW': u'Botswana',
|
|
||||||
u'WS': u'Samoa',
|
|
||||||
u'BR': u'Brazil',
|
|
||||||
u'BS': u'Bahamas',
|
|
||||||
u'BY': u'Belarus',
|
|
||||||
u'BZ': u'Belize',
|
|
||||||
u'RU': u'Russian Federation',
|
|
||||||
u'RW': u'Rwanda',
|
|
||||||
u'RE': u'Reunion',
|
|
||||||
u'TM': u'Turkmenistan',
|
|
||||||
u'TJ': u'Tajikistan',
|
|
||||||
u'RO': u'Romania',
|
|
||||||
u'TK': u'Tokelau',
|
|
||||||
u'GW': u'Guinea-Bissau',
|
|
||||||
u'GU': u'Guam',
|
|
||||||
u'GT': u'Guatemala',
|
|
||||||
u'GR': u'Greece',
|
|
||||||
u'GQ': u'Equatorial Guinea',
|
|
||||||
u'GP': u'Guadeloupe',
|
|
||||||
u'JP': u'Japan',
|
|
||||||
u'GY': u'Guyana',
|
|
||||||
u'GF': u'French Guiana',
|
|
||||||
u'GE': u'Georgia',
|
|
||||||
u'GD': u'Grenada',
|
|
||||||
u'GB': u'United Kingdom',
|
|
||||||
u'GA': u'Gabon',
|
|
||||||
u'SV': u'El Salvador',
|
|
||||||
u'GN': u'Guinea',
|
|
||||||
u'GM': u'Gambia',
|
|
||||||
u'GL': u'Greenland',
|
|
||||||
u'GI': u'Gibraltar',
|
|
||||||
u'GH': u'Ghana',
|
|
||||||
u'OM': u'Oman',
|
|
||||||
u'TN': u'Tunisia',
|
|
||||||
u'JO': u'Jordan',
|
|
||||||
u'HT': u'Haiti',
|
|
||||||
u'HU': u'Hungary',
|
|
||||||
u'HK': u'Hong Kong',
|
|
||||||
u'HN': u'Honduras',
|
|
||||||
u'HM': u'Heard and Mc Donald Islands',
|
|
||||||
u'VE': u'Venezuela',
|
|
||||||
u'PR': u'Puerto Rico',
|
|
||||||
u'PW': u'Palau',
|
|
||||||
u'PT': u'Portugal',
|
|
||||||
u'SJ': u'Svalbard and Jan Mayen Islands',
|
|
||||||
u'PY': u'Paraguay',
|
|
||||||
u'IQ': u'Iraq',
|
|
||||||
u'PA': u'Panama',
|
|
||||||
u'PF': u'French Polynesia',
|
|
||||||
u'PG': u'Papua New Guinea',
|
|
||||||
u'PE': u'Peru',
|
|
||||||
u'PK': u'Pakistan',
|
|
||||||
u'PH': u'Philippines',
|
|
||||||
u'PN': u'Pitcairn',
|
|
||||||
u'PL': u'Poland',
|
|
||||||
u'PM': u'St. Pierre and Miquelon',
|
|
||||||
u'ZM': u'Zambia',
|
|
||||||
u'EH': u'Western Sahara',
|
|
||||||
u'EE': u'Estonia',
|
|
||||||
u'EG': u'Egypt',
|
|
||||||
u'ZA': u'South Africa',
|
|
||||||
u'EC': u'Ecuador',
|
|
||||||
u'IT': u'Italy',
|
|
||||||
u'VN': u'Viet Nam',
|
|
||||||
u'SB': u'Solomon Islands',
|
|
||||||
u'ET': u'Ethiopia',
|
|
||||||
u'SO': u'Somalia',
|
|
||||||
u'ZW': u'Zimbabwe',
|
|
||||||
u'SA': u'Saudi Arabia',
|
|
||||||
u'ES': u'Spain',
|
|
||||||
u'ER': u'Eritrea',
|
|
||||||
u'MD': u'Moldova, Republic of',
|
|
||||||
u'MG': u'Madagascar',
|
|
||||||
u'MA': u'Morocco',
|
|
||||||
u'MC': u'Monaco',
|
|
||||||
u'UZ': u'Uzbekistan',
|
|
||||||
u'MM': u'Myanmar',
|
|
||||||
u'ML': u'Mali',
|
|
||||||
u'MO': u'Macau',
|
|
||||||
u'MN': u'Mongolia',
|
|
||||||
u'MH': u'Marshall Islands',
|
|
||||||
u'MK': u'Macedonia, The Former Yugoslav Republic of',
|
|
||||||
u'MU': u'Mauritius',
|
|
||||||
u'MT': u'Malta',
|
|
||||||
u'MW': u'Malawi',
|
|
||||||
u'MV': u'Maldives',
|
|
||||||
u'MQ': u'Martinique',
|
|
||||||
u'MP': u'Northern Mariana Islands',
|
|
||||||
u'MS': u'Montserrat',
|
|
||||||
u'MR': u'Mauritania',
|
|
||||||
u'UG': u'Uganda',
|
|
||||||
u'MY': u'Malaysia',
|
|
||||||
u'MX': u'Mexico',
|
|
||||||
u'IL': u'Israel',
|
|
||||||
u'FR': u'France',
|
|
||||||
u'IO': u'British Indian Ocean Territory',
|
|
||||||
u'SH': u'St. Helena',
|
|
||||||
u'FI': u'Finland',
|
|
||||||
u'FJ': u'Fiji',
|
|
||||||
u'FK': u'Falkland Islands (Malvinas)',
|
|
||||||
u'FM': u'Micronesia, Federated States of',
|
|
||||||
u'FO': u'Faroe Islands',
|
|
||||||
u'NI': u'Nicaragua',
|
|
||||||
u'NL': u'Netherlands',
|
|
||||||
u'NO': u'Norway',
|
|
||||||
u'NA': u'Namibia',
|
|
||||||
u'VU': u'Vanuatu',
|
|
||||||
u'NC': u'New Caledonia',
|
|
||||||
u'NE': u'Niger',
|
|
||||||
u'NF': u'Norfolk Island',
|
|
||||||
u'NG': u'Nigeria',
|
|
||||||
u'NZ': u'New Zealand',
|
|
||||||
u'ZR': u'Zaire',
|
|
||||||
u'NP': u'Nepal',
|
|
||||||
u'NR': u'Nauru',
|
|
||||||
u'NU': u'Niue',
|
|
||||||
u'CK': u'Cook Islands',
|
|
||||||
u'CI': u'Cote d\'Ivoire',
|
|
||||||
u'CH': u'Switzerland',
|
|
||||||
u'CO': u'Colombia',
|
|
||||||
u'CN': u'China',
|
|
||||||
u'CM': u'Cameroon',
|
|
||||||
u'CL': u'Chile',
|
|
||||||
u'CC': u'Cocos (Keeling) Islands',
|
|
||||||
u'CA': u'Canada',
|
|
||||||
u'CG': u'Congo',
|
|
||||||
u'CF': u'Central African Republic',
|
|
||||||
u'CZ': u'Czech Republic',
|
|
||||||
u'CY': u'Cyprus',
|
|
||||||
u'CX': u'Christmas Island',
|
|
||||||
u'CR': u'Costa Rica',
|
|
||||||
u'CV': u'Cape Verde',
|
|
||||||
u'CU': u'Cuba',
|
|
||||||
u'SZ': u'Swaziland',
|
|
||||||
u'SY': u'Syrian Arab Republic',
|
|
||||||
u'KG': u'Kyrgyzstan',
|
|
||||||
u'KE': u'Kenya',
|
|
||||||
u'SR': u'Suriname',
|
|
||||||
u'KI': u'Kiribati',
|
|
||||||
u'KH': u'Cambodia',
|
|
||||||
u'KN': u'Saint Kitts and Nevis',
|
|
||||||
u'KM': u'Comoros',
|
|
||||||
u'ST': u'Sao Tome and Principe',
|
|
||||||
u'SI': u'Slovenia',
|
|
||||||
u'KW': u'Kuwait',
|
|
||||||
u'SN': u'Senegal',
|
|
||||||
u'SM': u'San Marino',
|
|
||||||
u'SL': u'Sierra Leone',
|
|
||||||
u'SC': u'Seychelles',
|
|
||||||
u'KZ': u'Kazakhstan',
|
|
||||||
u'KY': u'Cayman Islands',
|
|
||||||
u'SG': u'Singapore',
|
|
||||||
u'SE': u'Sweden',
|
|
||||||
u'SD': u'Sudan',
|
|
||||||
u'DO': u'Dominican Republic',
|
|
||||||
u'DM': u'Dominica',
|
|
||||||
u'DJ': u'Djibouti',
|
|
||||||
u'DK': u'Denmark',
|
|
||||||
u'VG': u'Virgin Islands (British)',
|
|
||||||
u'DE': u'Germany',
|
|
||||||
u'YE': u'Yemen',
|
|
||||||
u'DZ': u'Algeria',
|
|
||||||
u'US': u'United States',
|
|
||||||
u'UY': u'Uruguay',
|
|
||||||
u'YT': u'Mayotte',
|
|
||||||
u'UM': u'United States Minor Outlying Islands',
|
|
||||||
u'LB': u'Lebanon',
|
|
||||||
u'LC': u'Saint Lucia',
|
|
||||||
u'LA': u'Lao People\'s Democratic Republic',
|
|
||||||
u'TV': u'Tuvalu',
|
|
||||||
u'TW': u'Taiwan',
|
|
||||||
u'TT': u'Trinidad and Tobago',
|
|
||||||
u'TR': u'Turkey',
|
|
||||||
u'LK': u'Sri Lanka',
|
|
||||||
u'LI': u'Liechtenstein',
|
|
||||||
u'LV': u'Latvia',
|
|
||||||
u'TO': u'Tonga',
|
|
||||||
u'LT': u'Lithuania',
|
|
||||||
u'LU': u'Luxembourg',
|
|
||||||
u'LR': u'Liberia',
|
|
||||||
u'LS': u'Lesotho',
|
|
||||||
u'TH': u'Thailand',
|
|
||||||
u'TF': u'French Southern Territories',
|
|
||||||
u'TG': u'Togo',
|
|
||||||
u'TD': u'Chad',
|
|
||||||
u'TC': u'Turks and Caicos Islands',
|
|
||||||
u'LY': u'Libyan Arab Jamahiriya',
|
|
||||||
u'VA': u'Vatican City State (Holy See)',
|
|
||||||
u'VC': u'Saint Vincent and The Grenadines',
|
|
||||||
u'AE': u'United Arab Emirates',
|
|
||||||
u'AD': u'Andorra',
|
|
||||||
u'AG': u'Antigua and Barbuda',
|
|
||||||
u'AF': u'Afghanistan',
|
|
||||||
u'AI': u'Anguilla',
|
|
||||||
u'VI': u'Virgin Islands (U.S.)',
|
|
||||||
u'IS': u'Iceland',
|
|
||||||
u'IR': u'Iran (Islamic Republic of)',
|
|
||||||
u'AM': u'Armenia',
|
|
||||||
u'AL': u'Albania',
|
|
||||||
u'AO': u'Angola',
|
|
||||||
u'AN': u'Netherlands Antilles',
|
|
||||||
u'AQ': u'Antarctica',
|
|
||||||
u'AS': u'American Samoa',
|
|
||||||
u'AR': u'Argentina',
|
|
||||||
u'AU': u'Australia',
|
|
||||||
u'AT': u'Austria',
|
|
||||||
u'AW': u'Aruba',
|
|
||||||
u'IN': u'India',
|
|
||||||
u'TZ': u'Tanzania, United Republic of',
|
|
||||||
u'AZ': u'Azerbaijan',
|
|
||||||
u'IE': u'Ireland',
|
|
||||||
u'ID': u'Indonesia',
|
|
||||||
u'UA': u'Ukraine',
|
|
||||||
u'QA': u'Qatar',
|
|
||||||
u'MZ': u'Mozambique',
|
|
||||||
u'BA': u'Bosnia and Herzegovina',
|
|
||||||
u'CD': u'Congo, The Democratic Republic of the',
|
|
||||||
u'CS': u'Serbia and Montenegro',
|
|
||||||
u'HR': u'Croatia',
|
|
||||||
u'KP': u'Korea (North), Democratic People\'s Republic of',
|
|
||||||
u'KR': u'Korea (South), Republic of',
|
|
||||||
u'SK': u'Slovakia',
|
|
||||||
u'SU': u'Soviet Union (historical, 1922-1991)',
|
|
||||||
u'TL': u'East Timor',
|
|
||||||
u'XC': u'Czechoslovakia (historical, 1918-1992)',
|
|
||||||
u'XE': u'Europe',
|
|
||||||
u'XG': u'East Germany (historical, 1949-1990)',
|
|
||||||
u'XU': u'[Unknown Country]',
|
|
||||||
u'XW': u'[Worldwide]',
|
|
||||||
u'YU': u'Yugoslavia (historical, 1918-1992)',
|
|
||||||
}
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__revision__ = '$Id: languagenames.py 8725 2006-12-17 22:39:07Z luks $'
|
|
||||||
|
|
||||||
languageNames = {
|
|
||||||
u'ART': u'Artificial (Other)',
|
|
||||||
u'ROH': u'Raeto-Romance',
|
|
||||||
u'SCO': u'Scots',
|
|
||||||
u'SCN': u'Sicilian',
|
|
||||||
u'ROM': u'Romany',
|
|
||||||
u'RON': u'Romanian',
|
|
||||||
u'OSS': u'Ossetian; Ossetic',
|
|
||||||
u'ALE': u'Aleut',
|
|
||||||
u'MNI': u'Manipuri',
|
|
||||||
u'NWC': u'Classical Newari; Old Newari; Classical Nepal Bhasa',
|
|
||||||
u'OSA': u'Osage',
|
|
||||||
u'MNC': u'Manchu',
|
|
||||||
u'MWR': u'Marwari',
|
|
||||||
u'VEN': u'Venda',
|
|
||||||
u'MWL': u'Mirandese',
|
|
||||||
u'FAS': u'Persian',
|
|
||||||
u'FAT': u'Fanti',
|
|
||||||
u'FAN': u'Fang',
|
|
||||||
u'FAO': u'Faroese',
|
|
||||||
u'DIN': u'Dinka',
|
|
||||||
u'HYE': u'Armenian',
|
|
||||||
u'DSB': u'Lower Sorbian',
|
|
||||||
u'CAR': u'Carib',
|
|
||||||
u'DIV': u'Divehi',
|
|
||||||
u'TEL': u'Telugu',
|
|
||||||
u'TEM': u'Timne',
|
|
||||||
u'NBL': u'Ndebele, South; South Ndebele',
|
|
||||||
u'TER': u'Tereno',
|
|
||||||
u'TET': u'Tetum',
|
|
||||||
u'SUN': u'Sundanese',
|
|
||||||
u'KUT': u'Kutenai',
|
|
||||||
u'SUK': u'Sukuma',
|
|
||||||
u'KUR': u'Kurdish',
|
|
||||||
u'KUM': u'Kumyk',
|
|
||||||
u'SUS': u'Susu',
|
|
||||||
u'NEW': u'Newari; Nepal Bhasa',
|
|
||||||
u'KUA': u'Kuanyama; Kwanyama',
|
|
||||||
u'MEN': u'Mende',
|
|
||||||
u'LEZ': u'Lezghian',
|
|
||||||
u'GLA': u'Gaelic; Scottish Gaelic',
|
|
||||||
u'BOS': u'Bosnian',
|
|
||||||
u'GLE': u'Irish',
|
|
||||||
u'EKA': u'Ekajuk',
|
|
||||||
u'GLG': u'Gallegan',
|
|
||||||
u'AKA': u'Akan',
|
|
||||||
u'BOD': u'Tibetan',
|
|
||||||
u'GLV': u'Manx',
|
|
||||||
u'JRB': u'Judeo-Arabic',
|
|
||||||
u'VIE': u'Vietnamese',
|
|
||||||
u'IPK': u'Inupiaq',
|
|
||||||
u'UZB': u'Uzbek',
|
|
||||||
u'BRE': u'Breton',
|
|
||||||
u'BRA': u'Braj',
|
|
||||||
u'AYM': u'Aymara',
|
|
||||||
u'CHA': u'Chamorro',
|
|
||||||
u'CHB': u'Chibcha',
|
|
||||||
u'CHE': u'Chechen',
|
|
||||||
u'CHG': u'Chagatai',
|
|
||||||
u'CHK': u'Chuukese',
|
|
||||||
u'CHM': u'Mari',
|
|
||||||
u'CHN': u'Chinook jargon',
|
|
||||||
u'CHO': u'Choctaw',
|
|
||||||
u'CHP': u'Chipewyan',
|
|
||||||
u'CHR': u'Cherokee',
|
|
||||||
u'CHU': u'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic',
|
|
||||||
u'CHV': u'Chuvash',
|
|
||||||
u'CHY': u'Cheyenne',
|
|
||||||
u'MSA': u'Malay',
|
|
||||||
u'III': u'Sichuan Yi',
|
|
||||||
u'ACE': u'Achinese',
|
|
||||||
u'IBO': u'Igbo',
|
|
||||||
u'IBA': u'Iban',
|
|
||||||
u'XHO': u'Xhosa',
|
|
||||||
u'DEU': u'German',
|
|
||||||
u'CAT': u'Catalan; Valencian',
|
|
||||||
u'DEL': u'Delaware',
|
|
||||||
u'DEN': u'Slave (Athapascan)',
|
|
||||||
u'CAD': u'Caddo',
|
|
||||||
u'TAT': u'Tatar',
|
|
||||||
u'RAJ': u'Rajasthani',
|
|
||||||
u'SPA': u'Spanish; Castilian',
|
|
||||||
u'TAM': u'Tamil',
|
|
||||||
u'TAH': u'Tahitian',
|
|
||||||
u'AFH': u'Afrihili',
|
|
||||||
u'ENG': u'English',
|
|
||||||
u'CSB': u'Kashubian',
|
|
||||||
u'NYN': u'Nyankole',
|
|
||||||
u'NYO': u'Nyoro',
|
|
||||||
u'SID': u'Sidamo',
|
|
||||||
u'NYA': u'Chichewa; Chewa; Nyanja',
|
|
||||||
u'SIN': u'Sinhala; Sinhalese',
|
|
||||||
u'AFR': u'Afrikaans',
|
|
||||||
u'LAM': u'Lamba',
|
|
||||||
u'SND': u'Sindhi',
|
|
||||||
u'MAR': u'Marathi',
|
|
||||||
u'LAH': u'Lahnda',
|
|
||||||
u'NYM': u'Nyamwezi',
|
|
||||||
u'SNA': u'Shona',
|
|
||||||
u'LAD': u'Ladino',
|
|
||||||
u'SNK': u'Soninke',
|
|
||||||
u'MAD': u'Madurese',
|
|
||||||
u'MAG': u'Magahi',
|
|
||||||
u'MAI': u'Maithili',
|
|
||||||
u'MAH': u'Marshallese',
|
|
||||||
u'LAV': u'Latvian',
|
|
||||||
u'MAL': u'Malayalam',
|
|
||||||
u'MAN': u'Mandingo',
|
|
||||||
u'ZND': u'Zande',
|
|
||||||
u'ZEN': u'Zenaga',
|
|
||||||
u'KBD': u'Kabardian',
|
|
||||||
u'ITA': u'Italian',
|
|
||||||
u'VAI': u'Vai',
|
|
||||||
u'TSN': u'Tswana',
|
|
||||||
u'TSO': u'Tsonga',
|
|
||||||
u'TSI': u'Tsimshian',
|
|
||||||
u'BYN': u'Blin; Bilin',
|
|
||||||
u'FIJ': u'Fijian',
|
|
||||||
u'FIN': u'Finnish',
|
|
||||||
u'EUS': u'Basque',
|
|
||||||
u'CEB': u'Cebuano',
|
|
||||||
u'DAN': u'Danish',
|
|
||||||
u'NOG': u'Nogai',
|
|
||||||
u'NOB': u'Norwegian Bokmål; Bokmål, Norwegian',
|
|
||||||
u'DAK': u'Dakota',
|
|
||||||
u'CES': u'Czech',
|
|
||||||
u'DAR': u'Dargwa',
|
|
||||||
u'DAY': u'Dayak',
|
|
||||||
u'NOR': u'Norwegian',
|
|
||||||
u'KPE': u'Kpelle',
|
|
||||||
u'GUJ': u'Gujarati',
|
|
||||||
u'MDF': u'Moksha',
|
|
||||||
u'MAS': u'Masai',
|
|
||||||
u'LAO': u'Lao',
|
|
||||||
u'MDR': u'Mandar',
|
|
||||||
u'GON': u'Gondi',
|
|
||||||
u'SMS': u'Skolt Sami',
|
|
||||||
u'SMO': u'Samoan',
|
|
||||||
u'SMN': u'Inari Sami',
|
|
||||||
u'SMJ': u'Lule Sami',
|
|
||||||
u'GOT': u'Gothic',
|
|
||||||
u'SME': u'Northern Sami',
|
|
||||||
u'BLA': u'Siksika',
|
|
||||||
u'SMA': u'Southern Sami',
|
|
||||||
u'GOR': u'Gorontalo',
|
|
||||||
u'AST': u'Asturian; Bable',
|
|
||||||
u'ORM': u'Oromo',
|
|
||||||
u'QUE': u'Quechua',
|
|
||||||
u'ORI': u'Oriya',
|
|
||||||
u'CRH': u'Crimean Tatar; Crimean Turkish',
|
|
||||||
u'ASM': u'Assamese',
|
|
||||||
u'PUS': u'Pushto',
|
|
||||||
u'DGR': u'Dogrib',
|
|
||||||
u'LTZ': u'Luxembourgish; Letzeburgesch',
|
|
||||||
u'NDO': u'Ndonga',
|
|
||||||
u'GEZ': u'Geez',
|
|
||||||
u'ISL': u'Icelandic',
|
|
||||||
u'LAT': u'Latin',
|
|
||||||
u'MAK': u'Makasar',
|
|
||||||
u'ZAP': u'Zapotec',
|
|
||||||
u'YID': u'Yiddish',
|
|
||||||
u'KOK': u'Konkani',
|
|
||||||
u'KOM': u'Komi',
|
|
||||||
u'KON': u'Kongo',
|
|
||||||
u'UKR': u'Ukrainian',
|
|
||||||
u'TON': u'Tonga (Tonga Islands)',
|
|
||||||
u'KOS': u'Kosraean',
|
|
||||||
u'KOR': u'Korean',
|
|
||||||
u'TOG': u'Tonga (Nyasa)',
|
|
||||||
u'HUN': u'Hungarian',
|
|
||||||
u'HUP': u'Hupa',
|
|
||||||
u'CYM': u'Welsh',
|
|
||||||
u'UDM': u'Udmurt',
|
|
||||||
u'BEJ': u'Beja',
|
|
||||||
u'BEN': u'Bengali',
|
|
||||||
u'BEL': u'Belarusian',
|
|
||||||
u'BEM': u'Bemba',
|
|
||||||
u'AAR': u'Afar',
|
|
||||||
u'NZI': u'Nzima',
|
|
||||||
u'SAH': u'Yakut',
|
|
||||||
u'SAN': u'Sanskrit',
|
|
||||||
u'SAM': u'Samaritan Aramaic',
|
|
||||||
u'SAG': u'Sango',
|
|
||||||
u'SAD': u'Sandawe',
|
|
||||||
u'RAR': u'Rarotongan',
|
|
||||||
u'RAP': u'Rapanui',
|
|
||||||
u'SAS': u'Sasak',
|
|
||||||
u'SAT': u'Santali',
|
|
||||||
u'MIN': u'Minangkabau',
|
|
||||||
u'LIM': u'Limburgan; Limburger; Limburgish',
|
|
||||||
u'LIN': u'Lingala',
|
|
||||||
u'LIT': u'Lithuanian',
|
|
||||||
u'EFI': u'Efik',
|
|
||||||
u'BTK': u'Batak (Indonesia)',
|
|
||||||
u'KAC': u'Kachin',
|
|
||||||
u'KAB': u'Kabyle',
|
|
||||||
u'KAA': u'Kara-Kalpak',
|
|
||||||
u'KAN': u'Kannada',
|
|
||||||
u'KAM': u'Kamba',
|
|
||||||
u'KAL': u'Kalaallisut; Greenlandic',
|
|
||||||
u'KAS': u'Kashmiri',
|
|
||||||
u'KAR': u'Karen',
|
|
||||||
u'KAU': u'Kanuri',
|
|
||||||
u'KAT': u'Georgian',
|
|
||||||
u'KAZ': u'Kazakh',
|
|
||||||
u'TYV': u'Tuvinian',
|
|
||||||
u'AWA': u'Awadhi',
|
|
||||||
u'URD': u'Urdu',
|
|
||||||
u'DOI': u'Dogri',
|
|
||||||
u'TPI': u'Tok Pisin',
|
|
||||||
u'MRI': u'Maori',
|
|
||||||
u'ABK': u'Abkhazian',
|
|
||||||
u'TKL': u'Tokelau',
|
|
||||||
u'NLD': u'Dutch; Flemish',
|
|
||||||
u'OJI': u'Ojibwa',
|
|
||||||
u'OCI': u'Occitan (post 1500); Provençal',
|
|
||||||
u'WOL': u'Wolof',
|
|
||||||
u'JAV': u'Javanese',
|
|
||||||
u'HRV': u'Croatian',
|
|
||||||
u'DYU': u'Dyula',
|
|
||||||
u'SSW': u'Swati',
|
|
||||||
u'MUL': u'Multiple languages',
|
|
||||||
u'HIL': u'Hiligaynon',
|
|
||||||
u'HIM': u'Himachali',
|
|
||||||
u'HIN': u'Hindi',
|
|
||||||
u'BAS': u'Basa',
|
|
||||||
u'GBA': u'Gbaya',
|
|
||||||
u'WLN': u'Walloon',
|
|
||||||
u'BAD': u'Banda',
|
|
||||||
u'NEP': u'Nepali',
|
|
||||||
u'CRE': u'Cree',
|
|
||||||
u'BAN': u'Balinese',
|
|
||||||
u'BAL': u'Baluchi',
|
|
||||||
u'BAM': u'Bambara',
|
|
||||||
u'BAK': u'Bashkir',
|
|
||||||
u'SHN': u'Shan',
|
|
||||||
u'ARP': u'Arapaho',
|
|
||||||
u'ARW': u'Arawak',
|
|
||||||
u'ARA': u'Arabic',
|
|
||||||
u'ARC': u'Aramaic',
|
|
||||||
u'ARG': u'Aragonese',
|
|
||||||
u'SEL': u'Selkup',
|
|
||||||
u'ARN': u'Araucanian',
|
|
||||||
u'LUS': u'Lushai',
|
|
||||||
u'MUS': u'Creek',
|
|
||||||
u'LUA': u'Luba-Lulua',
|
|
||||||
u'LUB': u'Luba-Katanga',
|
|
||||||
u'LUG': u'Ganda',
|
|
||||||
u'LUI': u'Luiseno',
|
|
||||||
u'LUN': u'Lunda',
|
|
||||||
u'LUO': u'Luo (Kenya and Tanzania)',
|
|
||||||
u'IKU': u'Inuktitut',
|
|
||||||
u'TUR': u'Turkish',
|
|
||||||
u'TUK': u'Turkmen',
|
|
||||||
u'TUM': u'Tumbuka',
|
|
||||||
u'COP': u'Coptic',
|
|
||||||
u'COS': u'Corsican',
|
|
||||||
u'COR': u'Cornish',
|
|
||||||
u'ILO': u'Iloko',
|
|
||||||
u'GWI': u'Gwich´in',
|
|
||||||
u'TLI': u'Tlingit',
|
|
||||||
u'TLH': u'Klingon; tlhIngan-Hol',
|
|
||||||
u'POR': u'Portuguese',
|
|
||||||
u'PON': u'Pohnpeian',
|
|
||||||
u'POL': u'Polish',
|
|
||||||
u'TGK': u'Tajik',
|
|
||||||
u'TGL': u'Tagalog',
|
|
||||||
u'FRA': u'French',
|
|
||||||
u'BHO': u'Bhojpuri',
|
|
||||||
u'SWA': u'Swahili',
|
|
||||||
u'DUA': u'Duala',
|
|
||||||
u'SWE': u'Swedish',
|
|
||||||
u'YAP': u'Yapese',
|
|
||||||
u'TIV': u'Tiv',
|
|
||||||
u'YAO': u'Yao',
|
|
||||||
u'XAL': u'Kalmyk',
|
|
||||||
u'FRY': u'Frisian',
|
|
||||||
u'GAY': u'Gayo',
|
|
||||||
u'OTA': u'Turkish, Ottoman (1500-1928)',
|
|
||||||
u'HMN': u'Hmong',
|
|
||||||
u'HMO': u'Hiri Motu',
|
|
||||||
u'GAA': u'Ga',
|
|
||||||
u'FUR': u'Friulian',
|
|
||||||
u'MLG': u'Malagasy',
|
|
||||||
u'SLV': u'Slovenian',
|
|
||||||
u'FIL': u'Filipino; Pilipino',
|
|
||||||
u'MLT': u'Maltese',
|
|
||||||
u'SLK': u'Slovak',
|
|
||||||
u'FUL': u'Fulah',
|
|
||||||
u'JPN': u'Japanese',
|
|
||||||
u'VOL': u'Volapük',
|
|
||||||
u'VOT': u'Votic',
|
|
||||||
u'IND': u'Indonesian',
|
|
||||||
u'AVE': u'Avestan',
|
|
||||||
u'JPR': u'Judeo-Persian',
|
|
||||||
u'AVA': u'Avaric',
|
|
||||||
u'PAP': u'Papiamento',
|
|
||||||
u'EWO': u'Ewondo',
|
|
||||||
u'PAU': u'Palauan',
|
|
||||||
u'EWE': u'Ewe',
|
|
||||||
u'PAG': u'Pangasinan',
|
|
||||||
u'PAM': u'Pampanga',
|
|
||||||
u'PAN': u'Panjabi; Punjabi',
|
|
||||||
u'KIR': u'Kirghiz',
|
|
||||||
u'NIA': u'Nias',
|
|
||||||
u'KIK': u'Kikuyu; Gikuyu',
|
|
||||||
u'SYR': u'Syriac',
|
|
||||||
u'KIN': u'Kinyarwanda',
|
|
||||||
u'NIU': u'Niuean',
|
|
||||||
u'EPO': u'Esperanto',
|
|
||||||
u'JBO': u'Lojban',
|
|
||||||
u'MIC': u'Mi\'kmaq; Micmac',
|
|
||||||
u'THA': u'Thai',
|
|
||||||
u'HAI': u'Haida',
|
|
||||||
u'ELL': u'Greek, Modern (1453-)',
|
|
||||||
u'ADY': u'Adyghe; Adygei',
|
|
||||||
u'ELX': u'Elamite',
|
|
||||||
u'ADA': u'Adangme',
|
|
||||||
u'GRB': u'Grebo',
|
|
||||||
u'HAT': u'Haitian; Haitian Creole',
|
|
||||||
u'HAU': u'Hausa',
|
|
||||||
u'HAW': u'Hawaiian',
|
|
||||||
u'BIN': u'Bini',
|
|
||||||
u'AMH': u'Amharic',
|
|
||||||
u'BIK': u'Bikol',
|
|
||||||
u'BIH': u'Bihari',
|
|
||||||
u'MOS': u'Mossi',
|
|
||||||
u'MOH': u'Mohawk',
|
|
||||||
u'MON': u'Mongolian',
|
|
||||||
u'MOL': u'Moldavian',
|
|
||||||
u'BIS': u'Bislama',
|
|
||||||
u'TVL': u'Tuvalu',
|
|
||||||
u'IJO': u'Ijo',
|
|
||||||
u'EST': u'Estonian',
|
|
||||||
u'KMB': u'Kimbundu',
|
|
||||||
u'UMB': u'Umbundu',
|
|
||||||
u'TMH': u'Tamashek',
|
|
||||||
u'FON': u'Fon',
|
|
||||||
u'HSB': u'Upper Sorbian',
|
|
||||||
u'RUN': u'Rundi',
|
|
||||||
u'RUS': u'Russian',
|
|
||||||
u'PLI': u'Pali',
|
|
||||||
u'SRD': u'Sardinian',
|
|
||||||
u'ACH': u'Acoli',
|
|
||||||
u'NDE': u'Ndebele, North; North Ndebele',
|
|
||||||
u'DZO': u'Dzongkha',
|
|
||||||
u'KRU': u'Kurukh',
|
|
||||||
u'SRR': u'Serer',
|
|
||||||
u'IDO': u'Ido',
|
|
||||||
u'SRP': u'Serbian',
|
|
||||||
u'KRO': u'Kru',
|
|
||||||
u'KRC': u'Karachay-Balkar',
|
|
||||||
u'NDS': u'Low German; Low Saxon; German, Low; Saxon, Low',
|
|
||||||
u'ZUN': u'Zuni',
|
|
||||||
u'ZUL': u'Zulu',
|
|
||||||
u'TWI': u'Twi',
|
|
||||||
u'NSO': u'Northern Sotho, Pedi; Sepedi',
|
|
||||||
u'SOM': u'Somali',
|
|
||||||
u'SON': u'Songhai',
|
|
||||||
u'SOT': u'Sotho, Southern',
|
|
||||||
u'MKD': u'Macedonian',
|
|
||||||
u'HER': u'Herero',
|
|
||||||
u'LOL': u'Mongo',
|
|
||||||
u'HEB': u'Hebrew',
|
|
||||||
u'LOZ': u'Lozi',
|
|
||||||
u'GIL': u'Gilbertese',
|
|
||||||
u'WAS': u'Washo',
|
|
||||||
u'WAR': u'Waray',
|
|
||||||
u'BUL': u'Bulgarian',
|
|
||||||
u'WAL': u'Walamo',
|
|
||||||
u'BUA': u'Buriat',
|
|
||||||
u'BUG': u'Buginese',
|
|
||||||
u'AZE': u'Azerbaijani',
|
|
||||||
u'ZHA': u'Zhuang; Chuang',
|
|
||||||
u'ZHO': u'Chinese',
|
|
||||||
u'NNO': u'Norwegian Nynorsk; Nynorsk, Norwegian',
|
|
||||||
u'UIG': u'Uighur; Uyghur',
|
|
||||||
u'MYV': u'Erzya',
|
|
||||||
u'INH': u'Ingush',
|
|
||||||
u'KHM': u'Khmer',
|
|
||||||
u'MYA': u'Burmese',
|
|
||||||
u'KHA': u'Khasi',
|
|
||||||
u'INA': u'Interlingua (International Auxiliary Language Association)',
|
|
||||||
u'NAH': u'Nahuatl',
|
|
||||||
u'TIR': u'Tigrinya',
|
|
||||||
u'NAP': u'Neapolitan',
|
|
||||||
u'NAV': u'Navajo; Navaho',
|
|
||||||
u'NAU': u'Nauru',
|
|
||||||
u'GRN': u'Guarani',
|
|
||||||
u'TIG': u'Tigre',
|
|
||||||
u'YOR': u'Yoruba',
|
|
||||||
u'ILE': u'Interlingue',
|
|
||||||
u'SQI': u'Albanian',
|
|
||||||
}
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__revision__ = '$Id: releasetypenames.py 8728 2006-12-17 23:42:30Z luks $'
|
|
||||||
|
|
||||||
releaseTypeNames = {
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#None': u'None',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Album': u'Album',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Single': u'Single',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#EP': u'EP',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Compilation': u'Compilation',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Soundtrack': u'Soundtrack',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Spokenword': u'Spokenword',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Interview': u'Interview',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Audiobook': u'Audiobook',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Live': u'Live',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Remix': u'Remix',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Other': u'Other',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Official': u'Official',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Promotion': u'Promotion',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Bootleg': u'Bootleg',
|
|
||||||
u'http://musicbrainz.org/ns/mmd-1.0#Pseudo-Release': u'Pseudo-Release',
|
|
||||||
}
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__revision__ = '$Id: scriptnames.py 7386 2006-04-30 11:12:55Z matt $'
|
|
||||||
|
|
||||||
scriptNames = {
|
|
||||||
u'Yiii': u'Yi',
|
|
||||||
u'Telu': u'Telugu',
|
|
||||||
u'Taml': u'Tamil',
|
|
||||||
u'Guru': u'Gurmukhi',
|
|
||||||
u'Hebr': u'Hebrew',
|
|
||||||
u'Geor': u'Georgian (Mkhedruli)',
|
|
||||||
u'Ugar': u'Ugaritic',
|
|
||||||
u'Cyrl': u'Cyrillic',
|
|
||||||
u'Hrkt': u'Kanji & Kana',
|
|
||||||
u'Armn': u'Armenian',
|
|
||||||
u'Runr': u'Runic',
|
|
||||||
u'Khmr': u'Khmer',
|
|
||||||
u'Latn': u'Latin',
|
|
||||||
u'Hani': u'Han (Hanzi, Kanji, Hanja)',
|
|
||||||
u'Ital': u'Old Italic (Etruscan, Oscan, etc.)',
|
|
||||||
u'Hano': u'Hanunoo (Hanunóo)',
|
|
||||||
u'Ethi': u'Ethiopic (Ge\'ez)',
|
|
||||||
u'Gujr': u'Gujarati',
|
|
||||||
u'Hang': u'Hangul',
|
|
||||||
u'Arab': u'Arabic',
|
|
||||||
u'Thaa': u'Thaana',
|
|
||||||
u'Buhd': u'Buhid',
|
|
||||||
u'Sinh': u'Sinhala',
|
|
||||||
u'Orya': u'Oriya',
|
|
||||||
u'Hans': u'Han (Simplified variant)',
|
|
||||||
u'Thai': u'Thai',
|
|
||||||
u'Cprt': u'Cypriot',
|
|
||||||
u'Linb': u'Linear B',
|
|
||||||
u'Hant': u'Han (Traditional variant)',
|
|
||||||
u'Osma': u'Osmanya',
|
|
||||||
u'Mong': u'Mongolian',
|
|
||||||
u'Deva': u'Devanagari (Nagari)',
|
|
||||||
u'Laoo': u'Lao',
|
|
||||||
u'Tagb': u'Tagbanwa',
|
|
||||||
u'Hira': u'Hiragana',
|
|
||||||
u'Bopo': u'Bopomofo',
|
|
||||||
u'Goth': u'Gothic',
|
|
||||||
u'Tale': u'Tai Le',
|
|
||||||
u'Mymr': u'Myanmar (Burmese)',
|
|
||||||
u'Tglg': u'Tagalog',
|
|
||||||
u'Grek': u'Greek',
|
|
||||||
u'Mlym': u'Malayalam',
|
|
||||||
u'Cher': u'Cherokee',
|
|
||||||
u'Tibt': u'Tibetan',
|
|
||||||
u'Kana': u'Katakana',
|
|
||||||
u'Syrc': u'Syriac',
|
|
||||||
u'Cans': u'Unified Canadian Aboriginal Syllabics',
|
|
||||||
u'Beng': u'Bengali',
|
|
||||||
u'Limb': u'Limbu',
|
|
||||||
u'Ogam': u'Ogham',
|
|
||||||
u'Knda': u'Kannada',
|
|
||||||
}
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
"""Utilities for working with Audio CDs.
|
|
||||||
|
|
||||||
This module contains utilities for working with Audio CDs.
|
|
||||||
|
|
||||||
The functions in this module need both a working ctypes package (already
|
|
||||||
included in python-2.5) and an installed libdiscid. If you don't have
|
|
||||||
libdiscid, it can't be loaded, or your platform isn't supported by either
|
|
||||||
ctypes or this module, a C{NotImplementedError} is raised when using the
|
|
||||||
L{readDisc()} function.
|
|
||||||
|
|
||||||
@author: Matthias Friedrich <matt@mafr.de>
|
|
||||||
"""
|
|
||||||
__revision__ = '$Id: disc.py 11987 2009-08-22 11:57:51Z matt $'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import urllib
|
|
||||||
import urlparse
|
|
||||||
import ctypes
|
|
||||||
import ctypes.util
|
|
||||||
from musicbrainz2.model import Disc
|
|
||||||
|
|
||||||
__all__ = [ 'DiscError', 'readDisc', 'getSubmissionUrl' ]
|
|
||||||
|
|
||||||
|
|
||||||
class DiscError(IOError):
|
|
||||||
"""The Audio CD could not be read.
|
|
||||||
|
|
||||||
This may be simply because no disc was in the drive, the device name
|
|
||||||
was wrong or the disc can't be read. Reading errors can occur in case
|
|
||||||
of a damaged disc or a copy protection mechanism, for example.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _openLibrary():
|
|
||||||
"""Tries to open libdiscid.
|
|
||||||
|
|
||||||
@return: a C{ctypes.CDLL} object, representing the opened library
|
|
||||||
|
|
||||||
@raise NotImplementedError: if the library can't be opened
|
|
||||||
"""
|
|
||||||
# This only works for ctypes >= 0.9.9.3. Any libdiscid is found,
|
|
||||||
# no matter how it's called on this platform.
|
|
||||||
try:
|
|
||||||
if hasattr(ctypes.cdll, 'find'):
|
|
||||||
libDiscId = ctypes.cdll.find('discid')
|
|
||||||
_setPrototypes(libDiscId)
|
|
||||||
return libDiscId
|
|
||||||
except OSError, e:
|
|
||||||
raise NotImplementedError('Error opening library: ' + str(e))
|
|
||||||
|
|
||||||
# Try to find the library using ctypes.util
|
|
||||||
libName = ctypes.util.find_library('discid')
|
|
||||||
if libName != None:
|
|
||||||
try:
|
|
||||||
libDiscId = ctypes.cdll.LoadLibrary(libName)
|
|
||||||
_setPrototypes(libDiscId)
|
|
||||||
return libDiscId
|
|
||||||
except OSError, e:
|
|
||||||
raise NotImplementedError('Error opening library: ' +
|
|
||||||
str(e))
|
|
||||||
|
|
||||||
# For compatibility with ctypes < 0.9.9.3 try to figure out the library
|
|
||||||
# name without the help of ctypes. We use cdll.LoadLibrary() below,
|
|
||||||
# which isn't available for ctypes == 0.9.9.3.
|
|
||||||
#
|
|
||||||
if sys.platform == 'linux2':
|
|
||||||
libName = 'libdiscid.so.0'
|
|
||||||
elif sys.platform == 'darwin':
|
|
||||||
libName = 'libdiscid.0.dylib'
|
|
||||||
elif sys.platform == 'win32':
|
|
||||||
libName = 'discid.dll'
|
|
||||||
else:
|
|
||||||
# This should at least work for Un*x-style operating systems
|
|
||||||
libName = 'libdiscid.so.0'
|
|
||||||
|
|
||||||
try:
|
|
||||||
libDiscId = ctypes.cdll.LoadLibrary(libName)
|
|
||||||
_setPrototypes(libDiscId)
|
|
||||||
return libDiscId
|
|
||||||
except OSError, e:
|
|
||||||
raise NotImplementedError('Error opening library: ' + str(e))
|
|
||||||
|
|
||||||
assert False # not reached
|
|
||||||
|
|
||||||
|
|
||||||
def _setPrototypes(libDiscId):
|
|
||||||
ct = ctypes
|
|
||||||
libDiscId.discid_new.argtypes = ( )
|
|
||||||
libDiscId.discid_new.restype = ct.c_void_p
|
|
||||||
|
|
||||||
libDiscId.discid_free.argtypes = (ct.c_void_p, )
|
|
||||||
|
|
||||||
libDiscId.discid_read.argtypes = (ct.c_void_p, ct.c_char_p)
|
|
||||||
|
|
||||||
libDiscId.discid_get_error_msg.argtypes = (ct.c_void_p, )
|
|
||||||
libDiscId.discid_get_error_msg.restype = ct.c_char_p
|
|
||||||
|
|
||||||
libDiscId.discid_get_id.argtypes = (ct.c_void_p, )
|
|
||||||
libDiscId.discid_get_id.restype = ct.c_char_p
|
|
||||||
|
|
||||||
libDiscId.discid_get_first_track_num.argtypes = (ct.c_void_p, )
|
|
||||||
libDiscId.discid_get_first_track_num.restype = ct.c_int
|
|
||||||
|
|
||||||
libDiscId.discid_get_last_track_num.argtypes = (ct.c_void_p, )
|
|
||||||
libDiscId.discid_get_last_track_num.restype = ct.c_int
|
|
||||||
|
|
||||||
libDiscId.discid_get_sectors.argtypes = (ct.c_void_p, )
|
|
||||||
libDiscId.discid_get_sectors.restype = ct.c_int
|
|
||||||
|
|
||||||
libDiscId.discid_get_track_offset.argtypes = (ct.c_void_p, ct.c_int)
|
|
||||||
libDiscId.discid_get_track_offset.restype = ct.c_int
|
|
||||||
|
|
||||||
libDiscId.discid_get_track_length.argtypes = (ct.c_void_p, ct.c_int)
|
|
||||||
libDiscId.discid_get_track_length.restype = ct.c_int
|
|
||||||
|
|
||||||
|
|
||||||
def getSubmissionUrl(disc, host='mm.musicbrainz.org', port=80):
|
|
||||||
"""Returns a URL for adding a disc to the MusicBrainz database.
|
|
||||||
|
|
||||||
A fully initialized L{musicbrainz2.model.Disc} object is needed, as
|
|
||||||
returned by L{readDisc}. A disc object returned by the web service
|
|
||||||
doesn't provide the necessary information.
|
|
||||||
|
|
||||||
Note that the created URL is intended for interactive use and points
|
|
||||||
to the MusicBrainz disc submission wizard by default. This method
|
|
||||||
just returns a URL, no network connection is needed. The disc drive
|
|
||||||
isn't used.
|
|
||||||
|
|
||||||
@param disc: a fully initialized L{musicbrainz2.model.Disc} object
|
|
||||||
@param host: a string containing a host name
|
|
||||||
@param port: an integer containing a port number
|
|
||||||
|
|
||||||
@return: a string containing the submission URL
|
|
||||||
|
|
||||||
@see: L{readDisc}
|
|
||||||
"""
|
|
||||||
assert isinstance(disc, Disc), 'musicbrainz2.model.Disc expected'
|
|
||||||
discid = disc.getId()
|
|
||||||
first = disc.getFirstTrackNum()
|
|
||||||
last = disc.getLastTrackNum()
|
|
||||||
sectors = disc.getSectors()
|
|
||||||
assert None not in (discid, first, last, sectors)
|
|
||||||
|
|
||||||
tracks = last - first + 1
|
|
||||||
toc = "%d %d %d " % (first, last, sectors)
|
|
||||||
toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) )
|
|
||||||
|
|
||||||
query = urllib.urlencode({ 'id': discid, 'toc': toc, 'tracks': tracks })
|
|
||||||
|
|
||||||
if port == 80:
|
|
||||||
netloc = host
|
|
||||||
else:
|
|
||||||
netloc = host + ':' + str(port)
|
|
||||||
|
|
||||||
url = ('http', netloc, '/bare/cdlookup.html', '', query, '')
|
|
||||||
|
|
||||||
return urlparse.urlunparse(url)
|
|
||||||
|
|
||||||
|
|
||||||
def readDisc(deviceName=None):
|
|
||||||
"""Reads an Audio CD in the disc drive.
|
|
||||||
|
|
||||||
This reads a CD's table of contents (TOC) and calculates the MusicBrainz
|
|
||||||
DiscID, which is a 28 character ASCII string. This DiscID can be used
|
|
||||||
to retrieve a list of matching releases from the web service (see
|
|
||||||
L{musicbrainz2.webservice.Query}).
|
|
||||||
|
|
||||||
Note that an Audio CD has to be in drive for this to work. The
|
|
||||||
C{deviceName} argument may be used to set the device. The default
|
|
||||||
depends on the operating system (on linux, it's C{'/dev/cdrom'}).
|
|
||||||
No network connection is needed for this function.
|
|
||||||
|
|
||||||
If the device doesn't exist or there's no valid Audio CD in the drive,
|
|
||||||
a L{DiscError} exception is raised.
|
|
||||||
|
|
||||||
@param deviceName: a string containing the CD drive's device name
|
|
||||||
|
|
||||||
@return: a L{musicbrainz2.model.Disc} object
|
|
||||||
|
|
||||||
@raise DiscError: if there was a problem reading the disc
|
|
||||||
@raise NotImplementedError: if DiscID generation isn't supported
|
|
||||||
"""
|
|
||||||
libDiscId = _openLibrary()
|
|
||||||
|
|
||||||
handle = libDiscId.discid_new()
|
|
||||||
assert handle != 0, "libdiscid: discid_new() returned NULL"
|
|
||||||
|
|
||||||
# Access the CD drive. This also works if deviceName is None because
|
|
||||||
# ctypes passes a NULL pointer in this case.
|
|
||||||
#
|
|
||||||
res = libDiscId.discid_read(handle, deviceName)
|
|
||||||
if res == 0:
|
|
||||||
raise DiscError(libDiscId.discid_get_error_msg(handle))
|
|
||||||
|
|
||||||
|
|
||||||
# Now extract the data from the result.
|
|
||||||
#
|
|
||||||
disc = Disc()
|
|
||||||
|
|
||||||
disc.setId( libDiscId.discid_get_id(handle) )
|
|
||||||
|
|
||||||
firstTrackNum = libDiscId.discid_get_first_track_num(handle)
|
|
||||||
lastTrackNum = libDiscId.discid_get_last_track_num(handle)
|
|
||||||
|
|
||||||
disc.setSectors(libDiscId.discid_get_sectors(handle))
|
|
||||||
|
|
||||||
for i in range(firstTrackNum, lastTrackNum+1):
|
|
||||||
trackOffset = libDiscId.discid_get_track_offset(handle, i)
|
|
||||||
trackSectors = libDiscId.discid_get_track_length(handle, i)
|
|
||||||
|
|
||||||
disc.addTrack( (trackOffset, trackSectors) )
|
|
||||||
|
|
||||||
disc.setFirstTrackNum(firstTrackNum)
|
|
||||||
disc.setLastTrackNum(lastTrackNum)
|
|
||||||
|
|
||||||
libDiscId.discid_free(handle)
|
|
||||||
|
|
||||||
return disc
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,204 +0,0 @@
|
|||||||
"""Various utilities to simplify common tasks.
|
|
||||||
|
|
||||||
This module contains helper functions to make common tasks easier.
|
|
||||||
|
|
||||||
@author: Matthias Friedrich <matt@mafr.de>
|
|
||||||
"""
|
|
||||||
__revision__ = '$Id: utils.py 11853 2009-07-21 09:26:50Z luks $'
|
|
||||||
|
|
||||||
import re
|
|
||||||
import urlparse
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'extractUuid', 'extractFragment', 'extractEntityType',
|
|
||||||
'getReleaseTypeName', 'getCountryName', 'getLanguageName',
|
|
||||||
'getScriptName',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# A pattern to split the path part of an absolute MB URI.
|
|
||||||
PATH_PATTERN = '^/(artist|release|track|label|release-group)/([^/]*)$'
|
|
||||||
|
|
||||||
|
|
||||||
def extractUuid(uriStr, resType=None):
|
|
||||||
"""Extract the UUID part from a MusicBrainz identifier.
|
|
||||||
|
|
||||||
This function takes a MusicBrainz ID (an absolute URI) as the input
|
|
||||||
and returns the UUID part of the URI, thus turning it into a relative
|
|
||||||
URI. If C{uriStr} is None or a relative URI, then it is returned
|
|
||||||
unchanged.
|
|
||||||
|
|
||||||
The C{resType} parameter can be used for error checking. Set it to
|
|
||||||
'artist', 'release', or 'track' to make sure C{uriStr} is a
|
|
||||||
syntactically valid MusicBrainz identifier of the given resource
|
|
||||||
type. If it isn't, a C{ValueError} exception is raised.
|
|
||||||
This error checking only works if C{uriStr} is an absolute URI, of
|
|
||||||
course.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> from musicbrainz2.utils import extractUuid
|
|
||||||
>>> extractUuid('http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5', 'artist')
|
|
||||||
'c0b2500e-0cef-4130-869d-732b23ed9df5'
|
|
||||||
>>>
|
|
||||||
|
|
||||||
@param uriStr: a string containing a MusicBrainz ID (an URI), or None
|
|
||||||
@param resType: a string containing a resource type
|
|
||||||
|
|
||||||
@return: a string containing a relative URI, or None
|
|
||||||
|
|
||||||
@raise ValueError: the given URI is no valid MusicBrainz ID
|
|
||||||
"""
|
|
||||||
if uriStr is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
(scheme, netloc, path) = urlparse.urlparse(uriStr)[:3]
|
|
||||||
|
|
||||||
if scheme == '':
|
|
||||||
return uriStr # no URI, probably already the UUID
|
|
||||||
|
|
||||||
if scheme != 'http' or netloc != 'musicbrainz.org':
|
|
||||||
raise ValueError('%s is no MB ID.' % uriStr)
|
|
||||||
|
|
||||||
m = re.match(PATH_PATTERN, path)
|
|
||||||
|
|
||||||
if m:
|
|
||||||
if resType is None:
|
|
||||||
return m.group(2)
|
|
||||||
else:
|
|
||||||
if m.group(1) == resType:
|
|
||||||
return m.group(2)
|
|
||||||
else:
|
|
||||||
raise ValueError('expected "%s" Id' % resType)
|
|
||||||
else:
|
|
||||||
raise ValueError('%s is no valid MB ID.' % uriStr)
|
|
||||||
|
|
||||||
|
|
||||||
def extractFragment(uriStr, uriPrefix=None):
|
|
||||||
"""Extract the fragment part from a URI.
|
|
||||||
|
|
||||||
If C{uriStr} is None or no absolute URI, then it is returned unchanged.
|
|
||||||
|
|
||||||
The C{uriPrefix} parameter can be used for error checking. If C{uriStr}
|
|
||||||
is an absolute URI, then the function checks if it starts with
|
|
||||||
C{uriPrefix}. If it doesn't, a C{ValueError} exception is raised.
|
|
||||||
|
|
||||||
@param uriStr: a string containing an absolute URI
|
|
||||||
@param uriPrefix: a string containing an URI prefix
|
|
||||||
|
|
||||||
@return: a string containing the fragment, or None
|
|
||||||
|
|
||||||
@raise ValueError: the given URI doesn't start with C{uriPrefix}
|
|
||||||
"""
|
|
||||||
if uriStr is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
(scheme, netloc, path, params, query, frag) = urlparse.urlparse(uriStr)
|
|
||||||
if scheme == '':
|
|
||||||
return uriStr # this is no URI
|
|
||||||
|
|
||||||
if uriPrefix is None or uriStr.startswith(uriPrefix):
|
|
||||||
return frag
|
|
||||||
else:
|
|
||||||
raise ValueError("prefix doesn't match URI %s" % uriStr)
|
|
||||||
|
|
||||||
|
|
||||||
def extractEntityType(uriStr):
|
|
||||||
"""Returns the entity type an entity URI is referring to.
|
|
||||||
|
|
||||||
@param uriStr: a string containing an absolute entity URI
|
|
||||||
|
|
||||||
@return: a string containing 'artist', 'release', 'track', or 'label'
|
|
||||||
|
|
||||||
@raise ValueError: if the given URI is no valid MusicBrainz ID
|
|
||||||
"""
|
|
||||||
if uriStr is None:
|
|
||||||
raise ValueError('None is no valid entity URI')
|
|
||||||
|
|
||||||
(scheme, netloc, path) = urlparse.urlparse(uriStr)[:3]
|
|
||||||
|
|
||||||
if scheme == '':
|
|
||||||
raise ValueError('%s is no absolute MB ID.' % uriStr)
|
|
||||||
|
|
||||||
if scheme != 'http' or netloc != 'musicbrainz.org':
|
|
||||||
raise ValueError('%s is no MB ID.' % uriStr)
|
|
||||||
|
|
||||||
m = re.match(PATH_PATTERN, path)
|
|
||||||
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
else:
|
|
||||||
raise ValueError('%s is no valid MB ID.' % uriStr)
|
|
||||||
|
|
||||||
|
|
||||||
def getReleaseTypeName(releaseType):
|
|
||||||
"""Returns the name of a release type URI.
|
|
||||||
|
|
||||||
@param releaseType: a string containing a release type URI
|
|
||||||
|
|
||||||
@return: a string containing a printable name for the release type
|
|
||||||
|
|
||||||
@see: L{musicbrainz2.model.Release}
|
|
||||||
"""
|
|
||||||
from musicbrainz2.data.releasetypenames import releaseTypeNames
|
|
||||||
return releaseTypeNames.get(releaseType)
|
|
||||||
|
|
||||||
|
|
||||||
def getCountryName(id_):
|
|
||||||
"""Returns a country's name based on an ISO-3166 country code.
|
|
||||||
|
|
||||||
The country table this function is based on has been modified for
|
|
||||||
MusicBrainz purposes by using the extension mechanism defined in
|
|
||||||
ISO-3166. All IDs are still valid ISO-3166 country codes, but some
|
|
||||||
IDs have been added to include historic countries and some of the
|
|
||||||
country names have been modified to make them better suited for
|
|
||||||
display purposes.
|
|
||||||
|
|
||||||
If the country ID is not found, None is returned. This may happen
|
|
||||||
for example, when new countries are added to the MusicBrainz web
|
|
||||||
service which aren't known to this library yet.
|
|
||||||
|
|
||||||
@param id_: a two-letter upper case string containing an ISO-3166 code
|
|
||||||
|
|
||||||
@return: a string containing the country's name, or None
|
|
||||||
|
|
||||||
@see: L{musicbrainz2.model}
|
|
||||||
"""
|
|
||||||
from musicbrainz2.data.countrynames import countryNames
|
|
||||||
return countryNames.get(id_)
|
|
||||||
|
|
||||||
|
|
||||||
def getLanguageName(id_):
|
|
||||||
"""Returns a language name based on an ISO-639-2/T code.
|
|
||||||
|
|
||||||
This function uses a subset of the ISO-639-2/T code table to map
|
|
||||||
language IDs (terminologic, not bibliographic ones!) to names.
|
|
||||||
|
|
||||||
@param id_: a three-letter upper case string containing an ISO-639-2/T code
|
|
||||||
|
|
||||||
@return: a string containing the language's name, or None
|
|
||||||
|
|
||||||
@see: L{musicbrainz2.model}
|
|
||||||
"""
|
|
||||||
from musicbrainz2.data.languagenames import languageNames
|
|
||||||
return languageNames.get(id_)
|
|
||||||
|
|
||||||
|
|
||||||
def getScriptName(id_):
|
|
||||||
"""Returns a script name based on an ISO-15924 code.
|
|
||||||
|
|
||||||
This function uses a subset of the ISO-15924 code table to map
|
|
||||||
script IDs to names.
|
|
||||||
|
|
||||||
@param id_: a four-letter string containing an ISO-15924 script code
|
|
||||||
|
|
||||||
@return: a string containing the script's name, or None
|
|
||||||
|
|
||||||
@see: L{musicbrainz2.model}
|
|
||||||
"""
|
|
||||||
from musicbrainz2.data.scriptnames import scriptNames
|
|
||||||
return scriptNames.get(id_)
|
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
|||||||
from pyItunes.Song import Song
|
|
||||||
import time
|
|
||||||
class Library:
|
|
||||||
def __init__(self,dictionary):
|
|
||||||
self.songs = self.parseDictionary(dictionary)
|
|
||||||
|
|
||||||
def parseDictionary(self,dictionary):
|
|
||||||
songs = []
|
|
||||||
format = "%Y-%m-%dT%H:%M:%SZ"
|
|
||||||
for song,attributes in dictionary.iteritems():
|
|
||||||
s = Song()
|
|
||||||
s.name = attributes.get('Name')
|
|
||||||
s.artist = attributes.get('Artist')
|
|
||||||
s.album_artist = attributes.get('Album Aritst')
|
|
||||||
s.composer = attributes.get('Composer')
|
|
||||||
s.album = attributes.get('Album')
|
|
||||||
s.genre = attributes.get('Genre')
|
|
||||||
s.kind = attributes.get('Kind')
|
|
||||||
if attributes.get('Size'):
|
|
||||||
s.size = int(attributes.get('Size'))
|
|
||||||
s.total_time = attributes.get('Total Time')
|
|
||||||
s.track_number = attributes.get('Track Number')
|
|
||||||
if attributes.get('Year'):
|
|
||||||
s.year = int(attributes.get('Year'))
|
|
||||||
if attributes.get('Date Modified'):
|
|
||||||
s.date_modified = time.strptime(attributes.get('Date Modified'),format)
|
|
||||||
if attributes.get('Date Added'):
|
|
||||||
s.date_added = time.strptime(attributes.get('Date Added'),format)
|
|
||||||
if attributes.get('Bit Rate'):
|
|
||||||
s.bit_rate = int(attributes.get('Bit Rate'))
|
|
||||||
if attributes.get('Sample Rate'):
|
|
||||||
s.sample_rate = int(attributes.get('Sample Rate'))
|
|
||||||
s.comments = attributes.get("Comments ")
|
|
||||||
if attributes.get('Rating'):
|
|
||||||
s.rating = int(attributes.get('Rating'))
|
|
||||||
if attributes.get('Play Count'):
|
|
||||||
s.play_count = int(attributes.get('Play Count'))
|
|
||||||
if attributes.get('Location'):
|
|
||||||
s.location = attributes.get('Location')
|
|
||||||
songs.append(s)
|
|
||||||
return songs
|
|
||||||
Binary file not shown.
@@ -1,46 +0,0 @@
|
|||||||
class Song:
|
|
||||||
"""
|
|
||||||
Song Attributes:
|
|
||||||
name (String)
|
|
||||||
artist (String)
|
|
||||||
album_arist (String)
|
|
||||||
composer = None (String)
|
|
||||||
album = None (String)
|
|
||||||
genre = None (String)
|
|
||||||
kind = None (String)
|
|
||||||
size = None (Integer)
|
|
||||||
total_time = None (Integer)
|
|
||||||
track_number = None (Integer)
|
|
||||||
year = None (Integer)
|
|
||||||
date_modified = None (Time)
|
|
||||||
date_added = None (Time)
|
|
||||||
bit_rate = None (Integer)
|
|
||||||
sample_rate = None (Integer)
|
|
||||||
comments = None (String)
|
|
||||||
rating = None (Integer)
|
|
||||||
album_rating = None (Integer)
|
|
||||||
play_count = None (Integer)
|
|
||||||
location = None (String)
|
|
||||||
"""
|
|
||||||
name = None
|
|
||||||
artist = None
|
|
||||||
album_arist = None
|
|
||||||
composer = None
|
|
||||||
album = None
|
|
||||||
genre = None
|
|
||||||
kind = None
|
|
||||||
size = None
|
|
||||||
total_time = None
|
|
||||||
track_number = None
|
|
||||||
year = None
|
|
||||||
date_modified = None
|
|
||||||
date_added = None
|
|
||||||
bit_rate = None
|
|
||||||
sample_rate = None
|
|
||||||
comments = None
|
|
||||||
rating = None
|
|
||||||
album_rating = None
|
|
||||||
play_count = None
|
|
||||||
location = None
|
|
||||||
|
|
||||||
#title = property(getTitle,setTitle)
|
|
||||||
Binary file not shown.
@@ -1,42 +0,0 @@
|
|||||||
import re
|
|
||||||
class XMLLibraryParser:
|
|
||||||
def __init__(self,xmlLibrary):
|
|
||||||
f = open(xmlLibrary)
|
|
||||||
s = f.read()
|
|
||||||
lines = s.split("\n")
|
|
||||||
self.dictionary = self.parser(lines)
|
|
||||||
|
|
||||||
def getValue(self,restOfLine):
|
|
||||||
value = re.sub("<.*?>","",restOfLine)
|
|
||||||
u = unicode(value,"utf-8")
|
|
||||||
cleanValue = u.encode("ascii","xmlcharrefreplace")
|
|
||||||
return cleanValue
|
|
||||||
|
|
||||||
def keyAndRestOfLine(self,line):
|
|
||||||
rawkey = re.search('<key>(.*?)</key>',line).group(0)
|
|
||||||
key = re.sub("</*key>","",rawkey)
|
|
||||||
restOfLine = re.sub("<key>.*?</key>","",line).strip()
|
|
||||||
return key,restOfLine
|
|
||||||
|
|
||||||
def parser(self,lines):
|
|
||||||
dicts = 0
|
|
||||||
songs = {}
|
|
||||||
inSong = False
|
|
||||||
for line in lines:
|
|
||||||
if re.search('<dict>',line):
|
|
||||||
dicts += 1
|
|
||||||
if re.search('</dict>',line):
|
|
||||||
dicts -= 1
|
|
||||||
inSong = False
|
|
||||||
songs[songkey] = temp
|
|
||||||
if dicts == 2 and re.search('<key>(.*?)</key>',line):
|
|
||||||
rawkey = re.search('<key>(.*?)</key>',line).group(0)
|
|
||||||
songkey = re.sub("</*key>","",rawkey)
|
|
||||||
inSong = True
|
|
||||||
temp = {}
|
|
||||||
if dicts == 3 and re.search('<key>(.*?)</key>',line):
|
|
||||||
key,restOfLine = self.keyAndRestOfLine(line)
|
|
||||||
temp[key] = self.getValue(restOfLine)
|
|
||||||
if len(songs) > 0 and dicts < 2:
|
|
||||||
return songs
|
|
||||||
return songs
|
|
||||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
|||||||
from pyItunes.XMLLibraryParser import XMLLibraryParser
|
|
||||||
from pyItunes.Library import Library
|
|
||||||
from pyItunes.Song import Song
|
|
||||||
Binary file not shown.
-212
@@ -1,212 +0,0 @@
|
|||||||
import urllib
|
|
||||||
from webServer import database
|
|
||||||
from headphones import config_file
|
|
||||||
from configobj import ConfigObj
|
|
||||||
import string
|
|
||||||
import feedparser
|
|
||||||
import sqlite3
|
|
||||||
import re
|
|
||||||
import logger
|
|
||||||
|
|
||||||
|
|
||||||
config = ConfigObj(config_file)
|
|
||||||
General = config['General']
|
|
||||||
NZBMatrix = config['NZBMatrix']
|
|
||||||
SABnzbd = config['SABnzbd']
|
|
||||||
Newznab = config['Newznab']
|
|
||||||
NZBsorg = config['NZBsorg']
|
|
||||||
usenet_retention = General['usenet_retention']
|
|
||||||
include_lossless = General['include_lossless']
|
|
||||||
nzbmatrix = NZBMatrix['nzbmatrix']
|
|
||||||
nzbmatrix_username = NZBMatrix['nzbmatrix_username']
|
|
||||||
nzbmatrix_apikey = NZBMatrix['nzbmatrix_apikey']
|
|
||||||
newznab = Newznab['newznab']
|
|
||||||
newznab_host = Newznab['newznab_host']
|
|
||||||
newznab_apikey = Newznab['newznab_apikey']
|
|
||||||
nzbsorg = NZBsorg['nzbsorg']
|
|
||||||
nzbsorg_uid = NZBsorg['nzbsorg_uid']
|
|
||||||
nzbsorg_hash = NZBsorg['nzbsorg_hash']
|
|
||||||
sab_host = SABnzbd['sab_host']
|
|
||||||
sab_username = SABnzbd['sab_username']
|
|
||||||
sab_password = SABnzbd['sab_password']
|
|
||||||
sab_apikey = SABnzbd['sab_apikey']
|
|
||||||
sab_category = SABnzbd['sab_category']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def searchNZB(albumid=None):
|
|
||||||
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
|
|
||||||
if albumid:
|
|
||||||
c.execute('SELECT ArtistName, AlbumTitle, AlbumID, ReleaseDate from albums WHERE Status="Wanted" AND AlbumID="%s"' % albumid)
|
|
||||||
else:
|
|
||||||
c.execute('SELECT ArtistName, AlbumTitle, AlbumID, ReleaseDate from albums WHERE Status="Wanted"')
|
|
||||||
|
|
||||||
results = c.fetchall()
|
|
||||||
|
|
||||||
for albums in results:
|
|
||||||
|
|
||||||
reldate = albums[3]
|
|
||||||
year = reldate[:4]
|
|
||||||
clname = string.replace(albums[0], ' & ', ' ')
|
|
||||||
clalbum = string.replace(albums[1], ' & ', ' ')
|
|
||||||
term1 = re.sub('[\.\-]', ' ', '%s %s %s' % (clname, clalbum, year)).encode('utf-8')
|
|
||||||
term = string.replace(term1, '"', '')
|
|
||||||
|
|
||||||
logger.log(u"Searching for "+term+" since it was marked as wanted")
|
|
||||||
|
|
||||||
resultlist = []
|
|
||||||
|
|
||||||
if nzbmatrix == '1':
|
|
||||||
|
|
||||||
if include_lossless == '1':
|
|
||||||
categories = "23,22"
|
|
||||||
maxsize = 2000000000
|
|
||||||
else:
|
|
||||||
categories = "22"
|
|
||||||
maxsize = 250000000
|
|
||||||
|
|
||||||
|
|
||||||
params = { "page": "download",
|
|
||||||
"username": nzbmatrix_username,
|
|
||||||
"apikey": nzbmatrix_apikey,
|
|
||||||
"subcat": categories,
|
|
||||||
"age": usenet_retention,
|
|
||||||
"english": 1,
|
|
||||||
"ssl": 1,
|
|
||||||
"scenename": 1,
|
|
||||||
"term": term
|
|
||||||
}
|
|
||||||
|
|
||||||
searchURL = "http://rss.nzbmatrix.com/rss.php?" + urllib.urlencode(params)
|
|
||||||
logger.log(u"Parsing results from "+searchURL)
|
|
||||||
d = feedparser.parse(searchURL)
|
|
||||||
|
|
||||||
for item in d.entries:
|
|
||||||
try:
|
|
||||||
url = item.link
|
|
||||||
title = item.title
|
|
||||||
size = int(item.links[1]['length'])
|
|
||||||
if size < maxsize:
|
|
||||||
resultlist.append((title, size, url))
|
|
||||||
logger.log(u"Found " + title +" : " + url + " (Size: " + size + ")")
|
|
||||||
else:
|
|
||||||
logger.log(title + u" is larger than the maxsize for this category, skipping. (Size: " + size+")", logger.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
except:
|
|
||||||
logger.log(u"No results found")
|
|
||||||
|
|
||||||
if newznab == '1':
|
|
||||||
|
|
||||||
if include_lossless == '1':
|
|
||||||
categories = "3040,3010"
|
|
||||||
maxsize = 2000000000
|
|
||||||
else:
|
|
||||||
categories = "3010"
|
|
||||||
maxsize = 250000000
|
|
||||||
|
|
||||||
params = { "t": "search",
|
|
||||||
"apikey": newznab_apikey,
|
|
||||||
"cat": categories,
|
|
||||||
"maxage": usenet_retention,
|
|
||||||
"q": term
|
|
||||||
}
|
|
||||||
|
|
||||||
searchURL = newznab_host + '/api?' + urllib.urlencode(params)
|
|
||||||
logger.log(u"Parsing results from "+searchURL)
|
|
||||||
|
|
||||||
d = feedparser.parse(searchURL)
|
|
||||||
|
|
||||||
for item in d.entries:
|
|
||||||
try:
|
|
||||||
url = item.link
|
|
||||||
title = item.title
|
|
||||||
size = int(item.links[1]['length'])
|
|
||||||
if size < maxsize:
|
|
||||||
resultlist.append((title, size, url))
|
|
||||||
logger.log(u"Found " + title +" : " + url + " (Size: " + size + ")")
|
|
||||||
else:
|
|
||||||
logger.log(title + u" is larger than the maxsize for this category, skipping. (Size: " + size+")", logger.WARNING)
|
|
||||||
|
|
||||||
except:
|
|
||||||
logger.log(u"No results found")
|
|
||||||
|
|
||||||
if nzbsorg == '1':
|
|
||||||
|
|
||||||
if include_lossless == '1':
|
|
||||||
categories = "5,3010"
|
|
||||||
maxsize = 2000000000
|
|
||||||
else:
|
|
||||||
categories = "5"
|
|
||||||
maxsize = 250000000
|
|
||||||
|
|
||||||
params = { "action": "search",
|
|
||||||
"dl": 1,
|
|
||||||
"i": nzbsorg_uid,
|
|
||||||
"h": nzbsorg_hash,
|
|
||||||
"age": usenet_retention,
|
|
||||||
"q": term
|
|
||||||
}
|
|
||||||
|
|
||||||
searchURL = 'https://secure.nzbs.org/rss.php?' + urllib.urlencode(params)
|
|
||||||
|
|
||||||
logger.log(u"Parsing results from "+searchURL)
|
|
||||||
d = feedparser.parse(searchURL)
|
|
||||||
|
|
||||||
for item in d.entries:
|
|
||||||
try:
|
|
||||||
url = item.link
|
|
||||||
title = item.title
|
|
||||||
size = int(item.links[1]['length'])
|
|
||||||
if size < maxsize:
|
|
||||||
resultlist.append((title, size, url))
|
|
||||||
logger.log(u"Found " + title +" : " + url + " (Size: " + size + ")")
|
|
||||||
else:
|
|
||||||
logger.log(title + u" is larger than the maxsize for this category, skipping. (Size: " + size +")", logger.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
except:
|
|
||||||
logger.log(u"No results found")
|
|
||||||
|
|
||||||
if len(resultlist):
|
|
||||||
bestqual = sorted(resultlist, key=lambda title: title[1], reverse=True)[0]
|
|
||||||
|
|
||||||
logger.log(u"Downloading: " + bestqual[0])
|
|
||||||
downloadurl = bestqual[2]
|
|
||||||
|
|
||||||
linkparams = {}
|
|
||||||
|
|
||||||
linkparams["mode"] = "addurl"
|
|
||||||
|
|
||||||
if sab_apikey != '':
|
|
||||||
linkparams["apikey"] = sab_apikey
|
|
||||||
if sab_username != '':
|
|
||||||
linkparams["ma_username"] = sab_username
|
|
||||||
if sab_password != '':
|
|
||||||
linkparams["ma_password"] = sab_password
|
|
||||||
if sab_category != '':
|
|
||||||
linkparams["cat"] = sab_category
|
|
||||||
|
|
||||||
linkparams["name"] = downloadurl
|
|
||||||
|
|
||||||
saburl = 'http://' + sab_host + '/sabnzbd/api?' + urllib.urlencode(linkparams)
|
|
||||||
logger.log(u"Sending link to SABNZBD: " + saburl)
|
|
||||||
|
|
||||||
try:
|
|
||||||
urllib.urlopen(saburl)
|
|
||||||
|
|
||||||
except:
|
|
||||||
logger.log(u"Unable to send link. Are you sure the host address is correct?", logger.ERROR)
|
|
||||||
|
|
||||||
c.execute('UPDATE albums SET status = "Snatched" WHERE AlbumID="%s"' % albums[2])
|
|
||||||
c.execute('INSERT INTO snatched VALUES( ?, ?, ?, ?, CURRENT_DATE, ?)', (albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched"))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from headphones import web_root
|
|
||||||
|
|
||||||
_header = '''
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Headphones</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="data/css/style.css" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="data/images/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" href="data/images/headphoneslogo.png" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">'''
|
|
||||||
|
|
||||||
_logobar = '''
|
|
||||||
<div class="logo"><a href="home"><img src="data/images/headphoneslogo.png" border="0">headphones<a></div>
|
|
||||||
<div class="search"><form action="findArtist" method="GET">
|
|
||||||
<input type="text" value="Add an artist" onfocus="if
|
|
||||||
(this.value==this.defaultValue) this.value='';" name="name" />
|
|
||||||
<input type="submit" /></form></div><br />
|
|
||||||
'''
|
|
||||||
|
|
||||||
_nav = '''<div class="nav">
|
|
||||||
<a href="home">HOME</a>
|
|
||||||
<a href="upcoming">UPCOMING</a>
|
|
||||||
<a href="manage">MANAGE</a>
|
|
||||||
<a href="history">HISTORY</a>
|
|
||||||
<a href="config">SETTINGS</a>
|
|
||||||
<div style="float:right">
|
|
||||||
<a href="restart" title="Restart"><img src="data/images/restart.png" height="15px" width="15px"></a>
|
|
||||||
<a href="shutdown" title="Shutdown"><img src="data/images/shutdown.png" height="15px" width="15px"></a>
|
|
||||||
</div>
|
|
||||||
</div>'''
|
|
||||||
|
|
||||||
_footer = '''
|
|
||||||
</div><div class="footer"><br /><div class="center"><form action="https://www.paypal.com/cgi-bin/webscr" method="post">
|
|
||||||
<input type="hidden" name="cmd" value="_s-xclick">
|
|
||||||
<input type="hidden" name="hosted_button_id" value="93FFC6WDV97QS">
|
|
||||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
|
|
||||||
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
|
||||||
</form>
|
|
||||||
</div></div>
|
|
||||||
</body>
|
|
||||||
</html>'''
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from cherrypy.process.plugins import SimplePlugin
|
|
||||||
from apscheduler.scheduler import Scheduler
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import Queue
|
|
||||||
|
|
||||||
class threadtool(SimplePlugin):
|
|
||||||
|
|
||||||
sched = Scheduler()
|
|
||||||
thread = None
|
|
||||||
|
|
||||||
def __init__(self, bus):
|
|
||||||
SimplePlugin.__init__(self, bus)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.running = True
|
|
||||||
if not self.thread:
|
|
||||||
self.thread = threading.Thread(target=self.run)
|
|
||||||
self.thread.start()
|
|
||||||
self.sched.start()
|
|
||||||
start.priority = 80
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.running = False
|
|
||||||
if self.thread:
|
|
||||||
self.thread.join()
|
|
||||||
self.thread = None
|
|
||||||
self.sched.shutdown()
|
|
||||||
stop.priority = 10
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import updater
|
|
||||||
import searcher
|
|
||||||
import mover
|
|
||||||
from webServer import database
|
|
||||||
if os.path.exists(database):
|
|
||||||
self.sched.add_cron_job(updater.dbUpdate, hour=4, minute=0, second=0)
|
|
||||||
self.sched.add_interval_job(searcher.searchNZB, hours=12)
|
|
||||||
#self.sched.add_interval_job(mover.moveFiles, minutes=10)
|
|
||||||
-76
@@ -1,76 +0,0 @@
|
|||||||
from webServer import database
|
|
||||||
import musicbrainz2.webservice as ws
|
|
||||||
import musicbrainz2.model as m
|
|
||||||
import musicbrainz2.utils as u
|
|
||||||
from mb import getReleaseGroup
|
|
||||||
import sqlite3
|
|
||||||
import time
|
|
||||||
|
|
||||||
import logger
|
|
||||||
|
|
||||||
def dbUpdate():
|
|
||||||
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('SELECT ArtistID, ArtistName from artists WHERE Status="Active"')
|
|
||||||
|
|
||||||
activeartists = c.fetchall()
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
|
|
||||||
while i < len(activeartists):
|
|
||||||
|
|
||||||
artistid = activeartists[i][0]
|
|
||||||
artistname = activeartists[i][1]
|
|
||||||
logger.log(u"Updating album information for artist: " + artistname)
|
|
||||||
|
|
||||||
c.execute('SELECT AlbumID from albums WHERE ArtistID="%s"' % artistid)
|
|
||||||
albumlist = c.fetchall()
|
|
||||||
|
|
||||||
inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), releaseGroups=True)
|
|
||||||
artist = ws.Query().getArtistById(artistid, inc)
|
|
||||||
|
|
||||||
for rg in artist.getReleaseGroups():
|
|
||||||
|
|
||||||
rgid = u.extractUuid(rg.id)
|
|
||||||
releaseid = getReleaseGroup(rgid)
|
|
||||||
inc = ws.ReleaseIncludes(artist=True, releaseEvents= True, tracks= True, releaseGroup=True)
|
|
||||||
results = ws.Query().getReleaseById(releaseid, inc)
|
|
||||||
|
|
||||||
if any(releaseid in x for x in albumlist):
|
|
||||||
|
|
||||||
logger.log(results.title + " already exists in the database. Updating ASIN, Release Date, Tracks")
|
|
||||||
|
|
||||||
c.execute('UPDATE albums SET AlbumASIN="%s", ReleaseDate="%s" WHERE AlbumID="%s"' % (results.asin, results.getEarliestReleaseDate(), u.extractUuid(results.id)))
|
|
||||||
|
|
||||||
for track in results.tracks:
|
|
||||||
c.execute('UPDATE tracks SET TrackDuration="%s" WHERE AlbumID="%s" AND TrackID="%s"' % (track.duration, u.extractUuid(results.id), u.extractUuid(track.id)))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
logger.log(u"New album found! Adding "+results.title+"to the database...")
|
|
||||||
c.execute('INSERT INTO albums VALUES( ?, ?, ?, ?, ?, CURRENT_DATE, ?, ?)', (artistid, results.artist.name, results.title, results.asin, results.getEarliestReleaseDate(), u.extractUuid(results.id), 'Skipped'))
|
|
||||||
conn.commit()
|
|
||||||
c.execute('SELECT ReleaseDate, DateAdded from albums WHERE AlbumID="%s"' % u.extractUuid(results.id))
|
|
||||||
|
|
||||||
latestrelease = c.fetchall()
|
|
||||||
|
|
||||||
if latestrelease[0][0] > latestrelease[0][1]:
|
|
||||||
|
|
||||||
c.execute('UPDATE albums SET Status = "Wanted" WHERE AlbumID="%s"' % u.extractUuid(results.id))
|
|
||||||
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for track in results.tracks:
|
|
||||||
|
|
||||||
c.execute('INSERT INTO tracks VALUES( ?, ?, ?, ?, ?, ?, ?, ?)', (artistid, results.artist.name, results.title, results.asin, u.extractUuid(results.id), track.title, track.duration, u.extractUuid(track.id)))
|
|
||||||
conn.commit()
|
|
||||||
time.sleep(1)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
-541
@@ -1,541 +0,0 @@
|
|||||||
import templates
|
|
||||||
import config
|
|
||||||
import cherrypy
|
|
||||||
import musicbrainz2.webservice as ws
|
|
||||||
import musicbrainz2.model as m
|
|
||||||
import musicbrainz2.utils as u
|
|
||||||
import os
|
|
||||||
import string
|
|
||||||
import time
|
|
||||||
import datetime
|
|
||||||
import sqlite3
|
|
||||||
import sys
|
|
||||||
import configobj
|
|
||||||
from headphones import FULL_PATH, config_file
|
|
||||||
from mb import getReleaseGroup
|
|
||||||
import logger
|
|
||||||
|
|
||||||
database = os.path.join(FULL_PATH, 'headphones.db')
|
|
||||||
|
|
||||||
class Headphones:
|
|
||||||
|
|
||||||
def index(self):
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
index.exposed=True
|
|
||||||
|
|
||||||
def home(self):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('SELECT ArtistName, ArtistID, Status from artists order by ArtistSortName collate nocase')
|
|
||||||
results = c.fetchall()
|
|
||||||
if len(results):
|
|
||||||
i = 0
|
|
||||||
page.append('''<div class="table"><table border="0" cellpadding="3">
|
|
||||||
<tr>
|
|
||||||
<th align="left" width="170">Artist Name</th>
|
|
||||||
<th align="center" width="100">Status</th>
|
|
||||||
<th align="center" width="300">Upcoming Albums</th>
|
|
||||||
<th> </th>
|
|
||||||
</tr>''')
|
|
||||||
while i < len(results):
|
|
||||||
c.execute('''SELECT AlbumTitle, ReleaseDate, DateAdded, AlbumID from albums WHERE ArtistID='%s' order by ReleaseDate DESC''' % results[i][1])
|
|
||||||
latestalbum = c.fetchall()
|
|
||||||
today = datetime.date.today()
|
|
||||||
if len(latestalbum) > 0:
|
|
||||||
if latestalbum[0][1] > datetime.date.isoformat(today):
|
|
||||||
newalbumName = '<a class="green" href="albumPage?AlbumID=%s"><i><b>%s</b></i>' % (latestalbum[0][3], latestalbum[0][0])
|
|
||||||
releaseDate = '(%s)</a>' % latestalbum[0][1]
|
|
||||||
else:
|
|
||||||
newalbumName = '<a class="gray" href="albumPage?AlbumID=%s"><i>%s</i>' % (latestalbum[0][3], latestalbum[0][0])
|
|
||||||
releaseDate = ""
|
|
||||||
if len(latestalbum) == 0:
|
|
||||||
newalbumName = '<font color="#CFCFCF">None</font>'
|
|
||||||
releaseDate = ""
|
|
||||||
if results[i][2] == 'Paused':
|
|
||||||
newStatus = '''<font color="red"><b>%s</b></font>(<A class="external" href="resumeArtist?ArtistID=%s">resume</a>)''' % (results[i][2], results[i][1])
|
|
||||||
else:
|
|
||||||
newStatus = '''%s(<A class="external" href="pauseArtist?ArtistID=%s">pause</a>)''' % (results[i][2], results[i][1])
|
|
||||||
page.append('''<tr><td align="left" width="300"><a href="artistPage?ArtistID=%s">%s</a>
|
|
||||||
(<A class="external" href="http://musicbrainz.org/artist/%s">link</a>) [<A class="externalred" href="deleteArtist?ArtistID=%s">delete</a>]</td>
|
|
||||||
<td align="center" width="160">%s</td>
|
|
||||||
<td align="center">%s %s</td></tr>''' % (results[i][1], results[i][0], results[i][1], results[i][1], newStatus, newalbumName, releaseDate))
|
|
||||||
i = i+1
|
|
||||||
c.close()
|
|
||||||
page.append('''</table></div>''')
|
|
||||||
page.append(templates._footer)
|
|
||||||
|
|
||||||
else:
|
|
||||||
page.append("""<div class="datanil">Add some artists to the database!</div>""")
|
|
||||||
return page
|
|
||||||
home.exposed = True
|
|
||||||
|
|
||||||
|
|
||||||
def artistPage(self, ArtistID):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('''SELECT ArtistName from artists WHERE ArtistID="%s"''' % ArtistID)
|
|
||||||
artistname = c.fetchall()
|
|
||||||
c.execute('''SELECT AlbumTitle, ReleaseDate, AlbumID, Status, ArtistName, AlbumASIN from albums WHERE ArtistID="%s" order by ReleaseDate DESC''' % ArtistID)
|
|
||||||
results = c.fetchall()
|
|
||||||
c.close()
|
|
||||||
i = 0
|
|
||||||
page.append('''<div class="table"><table border="0" cellpadding="3">
|
|
||||||
<tr><p align="center">%s <br /></p></tr>
|
|
||||||
<tr>
|
|
||||||
<th align="left" width="50"></th>
|
|
||||||
<th align="left" width="120">Album Name</th>
|
|
||||||
<th align="center" width="100">Release Date</th>
|
|
||||||
<th align="center" width="300">Status</th>
|
|
||||||
<th> </th>
|
|
||||||
</tr>''' % (artistname[0]))
|
|
||||||
while i < len(results):
|
|
||||||
if results[i][3] == 'Skipped':
|
|
||||||
newStatus = '''%s [<A class="external" href="queueAlbum?AlbumID=%s&ArtistID=%s">want</a>]''' % (results[i][3], results[i][2], ArtistID)
|
|
||||||
elif results[i][3] == 'Wanted':
|
|
||||||
newStatus = '''<b>%s</b>[<A class="external" href="unqueueAlbum?AlbumID=%s&ArtistID=%s">skip</a>]''' % (results[i][3], results[i][2], ArtistID)
|
|
||||||
elif results[i][3] == 'Downloaded':
|
|
||||||
newStatus = '''<b>%s</b>[<A class="external" href="queueAlbum?AlbumID=%s&ArtistID=%s">retry</a>]''' % (results[i][3], results[i][2], ArtistID)
|
|
||||||
elif results[i][3] == 'Snatched':
|
|
||||||
newStatus = '''<b>%s</b>[<A class="external" href="queueAlbum?AlbumID=%s&ArtistID=%s">retry</a>]''' % (results[i][3], results[i][2], ArtistID)
|
|
||||||
else:
|
|
||||||
newStatus = '%s' % (results[i][3])
|
|
||||||
page.append('''<tr><td align="left"><img src="http://ec1.images-amazon.com/images/P/%s.01.MZZZZZZZ.jpg" height="50" width="50"></td>
|
|
||||||
<td align="left" width="240"><a href="albumPage?AlbumID=%s">%s</a>
|
|
||||||
(<A class="external" href="http://musicbrainz.org/release/%s.html">link</a>)</td>
|
|
||||||
<td align="center" width="160">%s</td>
|
|
||||||
<td align="center">%s</td></tr>''' % (results[i][5], results[i][2], results[i][0], results[i][2], results[i][1], newStatus))
|
|
||||||
i = i+1
|
|
||||||
page.append('''</table></div>''')
|
|
||||||
page.append(templates._footer)
|
|
||||||
return page
|
|
||||||
artistPage.exposed = True
|
|
||||||
|
|
||||||
|
|
||||||
def albumPage(self, AlbumID):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('''SELECT ArtistID, ArtistName, AlbumTitle, TrackTitle, TrackDuration, TrackID, AlbumASIN from tracks WHERE AlbumID="%s"''' % AlbumID)
|
|
||||||
results = c.fetchall()
|
|
||||||
if results[0][6]:
|
|
||||||
albumart = '''<br /><img src="http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg" height="200" width="200"><br /><br />''' % results[0][6]
|
|
||||||
else:
|
|
||||||
albumart = ''
|
|
||||||
c.close()
|
|
||||||
i = 0
|
|
||||||
page.append('''<div class="table" align="center"><table border="0" cellpadding="3">
|
|
||||||
<tr><a href="artistPage?ArtistID=%s">%s</a> - %s<br />
|
|
||||||
<a href="queueAlbum?AlbumID=%s&ArtistID=%s">Download<br />%s</tr>
|
|
||||||
<br /><tr>
|
|
||||||
<th align="left" width="100">Track #</th>
|
|
||||||
<th align="left" width="100">Track Title</th>
|
|
||||||
<th align="center" width="300">Duration</th>
|
|
||||||
<th> </th>
|
|
||||||
</tr>''' % (results[0][0], results[0][1], results[0][2], AlbumID, results[0][0], albumart))
|
|
||||||
while i < len(results):
|
|
||||||
if results[i][4]:
|
|
||||||
duration = time.strftime("%M:%S", time.gmtime(int(results[i][4])/1000))
|
|
||||||
else:
|
|
||||||
duration = 'n/a'
|
|
||||||
page.append('''<tr><td align="left" width="120">%s</td>
|
|
||||||
<td align="left" width="240">%s (<A class="external" href="http://musicbrainz.org/recording/%s.html">link</a>)</td>
|
|
||||||
<td align="center">%s</td></tr>''' % (i+1, results[i][3], results[i][5], duration))
|
|
||||||
i = i+1
|
|
||||||
page.append('''</table></div>''')
|
|
||||||
|
|
||||||
|
|
||||||
page.append(templates._footer)
|
|
||||||
return page
|
|
||||||
|
|
||||||
albumPage.exposed = True
|
|
||||||
|
|
||||||
|
|
||||||
def findArtist(self, name):
|
|
||||||
|
|
||||||
page = [templates._header]
|
|
||||||
if len(name) == 0 or name == 'Add an artist':
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
else:
|
|
||||||
artistResults = ws.Query().getArtists(ws.ArtistFilter(string.replace(name, '&', '%38'), limit=8))
|
|
||||||
if len(artistResults) == 0:
|
|
||||||
logger.log(u"No results found for " + name)
|
|
||||||
page.append('''No results!<a class="blue" href="home">Go back</a>''')
|
|
||||||
return page
|
|
||||||
elif len(artistResults) > 1:
|
|
||||||
page.append('''Search returned multiple artists. Click the artist you want to add:<br /><br />''')
|
|
||||||
for result in artistResults:
|
|
||||||
artist = result.artist
|
|
||||||
detail = artist.getDisambiguation()
|
|
||||||
if detail:
|
|
||||||
disambiguation = '(%s)' % detail
|
|
||||||
else:
|
|
||||||
disambiguation = ''
|
|
||||||
page.append('''<a href="addArtist?artistid=%s">%s %s</a> (<a class="externalred" href="artistInfo?artistid=%s">more info</a>)<br />''' % (u.extractUuid(artist.id), artist.name, disambiguation, u.extractUuid(artist.id)))
|
|
||||||
return page
|
|
||||||
else:
|
|
||||||
for result in artistResults:
|
|
||||||
artist = result.artist
|
|
||||||
logger.log(u"Found one artist matching your search term: " + artist.name +" ("+ artist.id+")")
|
|
||||||
raise cherrypy.HTTPRedirect("addArtist?artistid=%s" % u.extractUuid(artist.id))
|
|
||||||
|
|
||||||
findArtist.exposed = True
|
|
||||||
|
|
||||||
def artistInfo(self, artistid):
|
|
||||||
page = [templates._header]
|
|
||||||
inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), releaseGroups=True)
|
|
||||||
artist = ws.Query().getArtistById(artistid, inc)
|
|
||||||
page.append('''Artist Name: %s </br> ''' % artist.name)
|
|
||||||
page.append('''Unique ID: %s </br></br>Albums:<br />''' % u.extractUuid(artist.id))
|
|
||||||
for rg in artist.getReleaseGroups():
|
|
||||||
page.append('''%s <br />''' % rg.title)
|
|
||||||
return page
|
|
||||||
|
|
||||||
artistInfo.exposed = True
|
|
||||||
|
|
||||||
def addArtist(self, artistid):
|
|
||||||
inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), releaseGroups=True)
|
|
||||||
artist = ws.Query().getArtistById(artistid, inc)
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('SELECT ArtistID from artists')
|
|
||||||
artistlist = c.fetchall()
|
|
||||||
if any(artistid in x for x in artistlist):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append('''%s has already been added. Go <a href="home">back</a>.''' % artist.name)
|
|
||||||
logger.log(artist.name + u" is already in the database!", logger.WARNING)
|
|
||||||
c.close()
|
|
||||||
return page
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.log(u"Adding " + artist.name + " to the database.")
|
|
||||||
c.execute('INSERT INTO artists VALUES( ?, ?, ?, CURRENT_DATE, ?)', (artistid, artist.name, artist.sortName, 'Active'))
|
|
||||||
for rg in artist.getReleaseGroups():
|
|
||||||
rgid = u.extractUuid(rg.id)
|
|
||||||
|
|
||||||
releaseid = getReleaseGroup(rgid)
|
|
||||||
|
|
||||||
inc = ws.ReleaseIncludes(artist=True, releaseEvents= True, tracks= True, releaseGroup=True)
|
|
||||||
results = ws.Query().getReleaseById(releaseid, inc)
|
|
||||||
|
|
||||||
logger.log(u"Now adding album: " + results.title+ " to the database")
|
|
||||||
c.execute('INSERT INTO albums VALUES( ?, ?, ?, ?, ?, CURRENT_DATE, ?, ?)', (artistid, results.artist.name, results.title, results.asin, results.getEarliestReleaseDate(), u.extractUuid(results.id), 'Skipped'))
|
|
||||||
c.execute('SELECT ReleaseDate, DateAdded from albums WHERE AlbumID="%s"' % u.extractUuid(results.id))
|
|
||||||
latestrelease = c.fetchall()
|
|
||||||
|
|
||||||
if latestrelease[0][0] > latestrelease[0][1]:
|
|
||||||
logger.log(results.title + u" is an upcoming album. Setting its status to 'Wanted'...")
|
|
||||||
c.execute('UPDATE albums SET Status = "Wanted" WHERE AlbumID="%s"' % u.extractUuid(results.id))
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for track in results.tracks:
|
|
||||||
c.execute('INSERT INTO tracks VALUES( ?, ?, ?, ?, ?, ?, ?, ?)', (artistid, results.artist.name, results.title, results.asin, u.extractUuid(results.id), track.title, track.duration, u.extractUuid(track.id)))
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
|
|
||||||
addArtist.exposed = True
|
|
||||||
|
|
||||||
def pauseArtist(self, ArtistID):
|
|
||||||
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
logger.log(u"Pausing artist: " + ArtistID)
|
|
||||||
c.execute('UPDATE artists SET status = "Paused" WHERE ArtistId="%s"' % ArtistID)
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
|
|
||||||
pauseArtist.exposed = True
|
|
||||||
|
|
||||||
def resumeArtist(self, ArtistID):
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
logger.log(u"Resuming artist: " + ArtistID)
|
|
||||||
c.execute('UPDATE artists SET status = "Active" WHERE ArtistId="%s"' % ArtistID)
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
|
|
||||||
resumeArtist.exposed = True
|
|
||||||
|
|
||||||
def deleteArtist(self, ArtistID):
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
logger.log(u"Deleting all traces of artist: " + ArtistID)
|
|
||||||
c.execute('''DELETE from artists WHERE ArtistID="%s"''' % ArtistID)
|
|
||||||
c.execute('''DELETE from albums WHERE ArtistID="%s"''' % ArtistID)
|
|
||||||
c.execute('''DELETE from tracks WHERE ArtistID="%s"''' % ArtistID)
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
|
|
||||||
deleteArtist.exposed = True
|
|
||||||
|
|
||||||
def queueAlbum(self, AlbumID, ArtistID):
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
logger.log(u"Marking album: " + AlbumID + "as wanted...")
|
|
||||||
c.execute('UPDATE albums SET status = "Wanted" WHERE AlbumID="%s"' % AlbumID)
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
import searcher
|
|
||||||
searcher.searchNZB(AlbumID)
|
|
||||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
|
||||||
|
|
||||||
queueAlbum.exposed = True
|
|
||||||
|
|
||||||
def unqueueAlbum(self, AlbumID, ArtistID):
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
logger.log(u"Marking album: " + AlbumID + "as skipped...")
|
|
||||||
c.execute('UPDATE albums SET status = "Skipped" WHERE AlbumID="%s"' % AlbumID)
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
|
||||||
|
|
||||||
unqueueAlbum.exposed = True
|
|
||||||
|
|
||||||
def upcoming(self):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
today = datetime.date.today()
|
|
||||||
todaysql = datetime.date.isoformat(today)
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('''SELECT AlbumTitle, ReleaseDate, DateAdded, AlbumASIN, AlbumID, ArtistName, ArtistID from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC''')
|
|
||||||
albums = c.fetchall()
|
|
||||||
page.append('''<div class="table"><table border="0" cellpadding="3">
|
|
||||||
<tr>
|
|
||||||
<th align="center" width="300"></th>
|
|
||||||
<th align="center" width="300"><div class="bigtext">Upcoming Albums<br /><br /></div></th>
|
|
||||||
<th align="center" width="300"></th>
|
|
||||||
<th> </th>
|
|
||||||
</tr>''')
|
|
||||||
if len(albums) == 0:
|
|
||||||
page.append("""</table><div class="center">No albums are coming out soon :(<br />
|
|
||||||
(try adding some more artists!)</div><table>""")
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(albums):
|
|
||||||
|
|
||||||
if albums[i][3]:
|
|
||||||
albumart = '''<br /><a href="http://www.amazon.com/dp/%s"><img src="http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg" height="200" width="200"></a><br /><br />''' % (albums[i][3], albums[i][3])
|
|
||||||
else:
|
|
||||||
albumart = 'No Album Art... yet.'
|
|
||||||
|
|
||||||
page.append('''<tr><td align="center" width="300">%s</td>
|
|
||||||
<td align="center" width="300"><a href="artistPage?ArtistID=%s">%s</a></td>
|
|
||||||
<td align="center" width="300"><a href="albumPage?AlbumID=%s"><i>%s</i> (%s)</a></td></tr>
|
|
||||||
''' % (albumart, albums[i][6], albums[i][5], albums[i][4], albums[i][0], albums[i][1]))
|
|
||||||
i += 1
|
|
||||||
page.append('''</table></div>''')
|
|
||||||
if len(albums):
|
|
||||||
page.append(templates._footer)
|
|
||||||
|
|
||||||
return page
|
|
||||||
upcoming.exposed = True
|
|
||||||
|
|
||||||
def manage(self):
|
|
||||||
config = configobj.ConfigObj(config_file)
|
|
||||||
try:
|
|
||||||
path = config['General']['path_to_xml']
|
|
||||||
except:
|
|
||||||
path = 'Absolute path to iTunes XML or Top-Level Music Directory'
|
|
||||||
try:
|
|
||||||
path2 = config['General']['path_to_itunes']
|
|
||||||
except:
|
|
||||||
path2 = 'Enter a directory to scan'
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
page.append('''
|
|
||||||
<div class="table"><div class="config"><h1>Scan Music Library</h1><br />
|
|
||||||
Where do you keep your music?<br /><br />
|
|
||||||
You can put in any directory, and it will scan for audio files in that folder
|
|
||||||
(including all subdirectories)<br /><br /> For example: '/Users/name/Music'
|
|
||||||
<br /> <br />
|
|
||||||
It may take a while depending on how many files you have. You can navigate away from the page<br />
|
|
||||||
as soon as you click 'Submit'
|
|
||||||
<br /><br />
|
|
||||||
|
|
||||||
<form action="musicScan" method="GET" align="center">
|
|
||||||
<input type="text" value="%s" onfocus="if
|
|
||||||
(this.value==this.defaultValue) this.value='';" name="path" size="70" />
|
|
||||||
<input type="submit" /></form><br /><br /></div></div>
|
|
||||||
<div class="table"><div class="config"><h1>Import or Sync Your iTunes Library/Music Folder</h1><br />
|
|
||||||
This is here for legacy purposes (try the Music Scanner above!) <br /><br />
|
|
||||||
If you'd rather import an iTunes .xml file, you can enter the full path here. <br /><br />
|
|
||||||
<form action="importItunes" method="GET" align="center">
|
|
||||||
<input type="text" value="%s" onfocus="if
|
|
||||||
(this.value==this.defaultValue) this.value='';" name="path" size="70" />
|
|
||||||
<input type="submit" /></form><br /><br /></div></div>
|
|
||||||
<div class="table"><div class="config"><h1>Force Search</h1><br />
|
|
||||||
<a href="forceSearch">Force Check for Wanted Albums</a><br /><br />
|
|
||||||
<a href="forceUpdate">Force Update Active Artists </a><br /><br /><br /></div></div>''' % (path2, path))
|
|
||||||
page.append(templates._footer)
|
|
||||||
return page
|
|
||||||
manage.exposed = True
|
|
||||||
|
|
||||||
def importItunes(self, path):
|
|
||||||
config = configobj.ConfigObj(config_file)
|
|
||||||
config['General']['path_to_xml'] = path
|
|
||||||
config.write()
|
|
||||||
import itunesimport
|
|
||||||
itunesimport.itunesImport(path)
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
importItunes.exposed = True
|
|
||||||
|
|
||||||
def musicScan(self, path):
|
|
||||||
config = configobj.ConfigObj(config_file)
|
|
||||||
config['General']['path_to_itunes'] = path
|
|
||||||
config.write()
|
|
||||||
import itunesimport
|
|
||||||
itunesimport.scanMusic(path)
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
musicScan.exposed = True
|
|
||||||
|
|
||||||
def forceUpdate(self):
|
|
||||||
import updater
|
|
||||||
updater.dbUpdate()
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
forceUpdate.exposed = True
|
|
||||||
|
|
||||||
def forceSearch(self):
|
|
||||||
import searcher
|
|
||||||
searcher.searchNZB()
|
|
||||||
raise cherrypy.HTTPRedirect("home")
|
|
||||||
forceSearch.exposed = True
|
|
||||||
|
|
||||||
|
|
||||||
def history(self):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
c.execute('''SELECT AlbumID, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT from snatched order by DateAdded DESC''')
|
|
||||||
snatched = c.fetchall()
|
|
||||||
page.append('''<div class="table"><table border="0" cellpadding="3">
|
|
||||||
<tr>
|
|
||||||
<th align="center" width="300"></th>
|
|
||||||
<th align="center" width="300"><div class="bigtext">History <a class="external" href="clearhistory">clear all</a><br /><br /></div></th>
|
|
||||||
<th align="center" width="300"></th>
|
|
||||||
<th> </th>
|
|
||||||
</tr>''')
|
|
||||||
if len(snatched) == 0:
|
|
||||||
page.append("""</table><div class="center"></div><table>""")
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(snatched):
|
|
||||||
mb = snatched[i][2] / 1048576
|
|
||||||
size = '%.2fM' % mb
|
|
||||||
page.append('''<tr><td align="center" width="300">%s</td>
|
|
||||||
<td align="center" width="300">%s</td>
|
|
||||||
<td align="center" width="300">%s</td>
|
|
||||||
<td align="center" width="300">%s</td>
|
|
||||||
</tr>
|
|
||||||
''' % (snatched[i][5], snatched[i][1], size, snatched[i][4]))
|
|
||||||
i += 1
|
|
||||||
page.append('''</table></div>''')
|
|
||||||
if len(snatched):
|
|
||||||
page.append(templates._footer)
|
|
||||||
return page
|
|
||||||
history.exposed = True
|
|
||||||
|
|
||||||
def clearhistory(self):
|
|
||||||
conn=sqlite3.connect(database)
|
|
||||||
c=conn.cursor()
|
|
||||||
logger.log(u"Clearing history")
|
|
||||||
c.execute('''DELETE from snatched''')
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
raise cherrypy.HTTPRedirect("history")
|
|
||||||
clearhistory.exposed = True
|
|
||||||
|
|
||||||
def config(self):
|
|
||||||
page = [templates._header]
|
|
||||||
page.append(templates._logobar)
|
|
||||||
page.append(templates._nav)
|
|
||||||
page.append(config.form)
|
|
||||||
#page.append(templates._footer)
|
|
||||||
return page
|
|
||||||
|
|
||||||
config.exposed = True
|
|
||||||
|
|
||||||
|
|
||||||
def configUpdate(self, http_host='127.0.0.1', http_username=None, http_port=8181, http_password=None, launch_browser=0,
|
|
||||||
sab_host=None, sab_username=None, sab_apikey=None, sab_password=None, sab_category=None, music_download_dir=None,
|
|
||||||
usenet_retention=None, nzbmatrix=0, nzbmatrix_username=None, nzbmatrix_apikey=None, newznab=0, newznab_host=None, newznab_apikey=None,
|
|
||||||
nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, include_lossless=0,flac_to_mp3=0, move_to_itunes=0, path_to_itunes=None, rename_mp3s=0, cleanup=0, add_album_art=0):
|
|
||||||
|
|
||||||
configs = configobj.ConfigObj(config_file)
|
|
||||||
SABnzbd = configs['SABnzbd']
|
|
||||||
General = configs['General']
|
|
||||||
NZBMatrix = configs['NZBMatrix']
|
|
||||||
Newznab = configs['Newznab']
|
|
||||||
NZBsorg = configs['NZBsorg']
|
|
||||||
General['http_host'] = http_host
|
|
||||||
General['http_port'] = http_port
|
|
||||||
General['http_username'] = http_username
|
|
||||||
General['http_password'] = http_password
|
|
||||||
General['launch_browser'] = launch_browser
|
|
||||||
SABnzbd['sab_host'] = sab_host
|
|
||||||
SABnzbd['sab_username'] = sab_username
|
|
||||||
SABnzbd['sab_password'] = sab_password
|
|
||||||
SABnzbd['sab_apikey'] = sab_apikey
|
|
||||||
SABnzbd['sab_category'] = sab_category
|
|
||||||
General['music_download_dir'] = music_download_dir
|
|
||||||
General['usenet_retention'] = usenet_retention
|
|
||||||
NZBMatrix['nzbmatrix'] = nzbmatrix
|
|
||||||
NZBMatrix['nzbmatrix_username'] = nzbmatrix_username
|
|
||||||
NZBMatrix['nzbmatrix_apikey'] = nzbmatrix_apikey
|
|
||||||
Newznab['newznab'] = newznab
|
|
||||||
Newznab['newznab_host'] = newznab_host
|
|
||||||
Newznab['newznab_apikey'] = newznab_apikey
|
|
||||||
NZBsorg['nzbsorg'] = nzbsorg
|
|
||||||
NZBsorg['nzbsorg_uid'] = nzbsorg_uid
|
|
||||||
NZBsorg['nzbsorg_hash'] = nzbsorg_hash
|
|
||||||
General['include_lossless'] = include_lossless
|
|
||||||
General['flac_to_mp3'] = flac_to_mp3
|
|
||||||
General['move_to_itunes'] = move_to_itunes
|
|
||||||
General['path_to_itunes'] = path_to_itunes
|
|
||||||
General['rename_mp3s'] = rename_mp3s
|
|
||||||
General['cleanup'] = cleanup
|
|
||||||
General['add_album_art'] = add_album_art
|
|
||||||
|
|
||||||
configs.write()
|
|
||||||
reload(config)
|
|
||||||
raise cherrypy.HTTPRedirect("config")
|
|
||||||
|
|
||||||
|
|
||||||
configUpdate.exposed = True
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
shutdown.exposed = True
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
logger.log(u"Restarting Headphones.")
|
|
||||||
restart = True
|
|
||||||
#answer = raw_input("Do you want to restart this program ? ")
|
|
||||||
#if answer.strip() in "y Y yes Yes YES".split():
|
|
||||||
#restart = True
|
|
||||||
if restart:
|
|
||||||
python = sys.executable
|
|
||||||
os.execl(python, python, * sys.argv)
|
|
||||||
|
|
||||||
restart.exposed = True
|
|
||||||
Reference in New Issue
Block a user