pax_global_header 0000666 0000000 0000000 00000000064 13204006733 0014510 g ustar 00root root 0000000 0000000 52 comment=489c25825d76436efb4ea31102bc559543428899
woob-489c25825d76436efb4ea31102bc559543428899-weboob/ 0000775 0000000 0000000 00000000000 13204006733 0020320 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/ 0000775 0000000 0000000 00000000000 13204006733 0021575 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0023674 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/ 0000775 0000000 0000000 00000000000 13204006733 0024263 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0026362 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobank/ 0000775 0000000 0000000 00000000000 13204006733 0025676 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobank/__init__.py 0000664 0000000 0000000 00000001426 13204006733 0030012 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobank import Boobank
__all__ = ['Boobank']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobank/boobank.py 0000664 0000000 0000000 00000074304 13204006733 0027673 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2009-2011 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from contextlib import contextmanager
import datetime
import uuid
from dateutil.relativedelta import relativedelta
from dateutil.parser import parse as parse_date
from decimal import Decimal, InvalidOperation
from weboob.browser.browsers import APIBrowser
from weboob.browser.profiles import Weboob
from weboob.exceptions import BrowserHTTPError
from weboob.capabilities.base import empty, find_object
from weboob.capabilities.bank import CapBank, Account, Transaction, CapBankTransfer, \
Transfer, TransferStep, Recipient, AddRecipientStep
from weboob.capabilities.contact import CapContact, Advisor
from weboob.capabilities.profile import CapProfile
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Boobank']
class OfxFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'rdate', 'label', 'raw', 'amount', 'category')
TYPES_ACCTS = {
Account.TYPE_CHECKING: 'CHECKING',
Account.TYPE_SAVINGS: 'SAVINGS',
Account.TYPE_DEPOSIT: 'DEPOSIT',
Account.TYPE_LOAN: 'LOAN',
Account.TYPE_MARKET: 'MARKET',
Account.TYPE_JOINT: 'JOINT',
Account.TYPE_CARD: 'CARD',
}
TYPES_TRANS = {
Transaction.TYPE_TRANSFER: 'DIRECTDEP',
Transaction.TYPE_ORDER: 'PAYMENT',
Transaction.TYPE_CHECK: 'CHECK',
Transaction.TYPE_DEPOSIT: 'DEP',
Transaction.TYPE_PAYBACK: 'OTHER',
Transaction.TYPE_WITHDRAWAL: 'ATM',
Transaction.TYPE_CARD: 'POS',
Transaction.TYPE_LOAN_PAYMENT: 'INT',
Transaction.TYPE_BANK: 'FEE',
}
balance = Decimal(0)
coming = Decimal(0)
seen = set()
def start_format(self, **kwargs):
account = kwargs['account']
self.balance = account.balance
self.coming = account.coming
self.output(u'OFXHEADER:100')
self.output(u'DATA:OFXSGML')
self.output(u'VERSION:102')
self.output(u'SECURITY:NONE')
self.output(u'ENCODING:USASCII')
self.output(u'CHARSET:1252')
self.output(u'COMPRESSION:NONE')
self.output(u'OLDFILEUID:NONE')
self.output(u'NEWFILEUID:%s\n' % uuid.uuid1())
self.output(u'0INFO')
self.output(u'%s113942ENG' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'%s' % uuid.uuid1())
self.output(u'0INFOnull')
self.output(u'%s' % (account.currency or 'EUR'))
self.output(u'null')
self.output(u'null')
self.output(u'%s' % account.id)
account_type = self.TYPES_ACCTS.get(account.type, 'CHECKING')
self.output(u'%s' % account_type)
self.output(u'null')
self.output('')
self.output(u'')
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
def format_obj(self, obj, alias):
# special case of coming operations with card ID
result = u'\n'
if hasattr(obj, '_coming') and obj._coming and hasattr(obj, 'obj._cardid') and not empty(obj._cardid):
result += u'%s\n' % obj._cardid
elif obj.type in self.TYPES_TRANS:
result += u'%s\n' % self.TYPES_TRANS[obj.type]
else:
result += u'%s\n' % ('DEBIT' if obj.amount < 0 else 'CREDIT')
result += u'%s\n' % obj.date.strftime('%Y%m%d')
if obj.rdate:
result += u'%s\n' % obj.rdate.strftime('%Y%m%d')
result += u'%s\n' % obj.amount
result += u'%s\n' % obj.unique_id(self.seen)
if hasattr(obj, 'label') and not empty(obj.label):
result += u'%s\n' % obj.label.replace('&', '&')
else:
result += u'%s\n' % obj.raw.replace('&', '&')
if obj.category:
result += u'%s\n' % obj.category.replace('&', '&')
result += u''
return result
def flush(self):
self.output(u'')
self.output(u'%s' % self.balance)
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'')
try:
self.output(u'%s' % (self.balance + self.coming))
except TypeError:
self.output(u'%s' % self.balance)
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'')
class QifFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'raw', 'amount')
def start_format(self, **kwargs):
self.output(u'!Type:Bank')
def format_obj(self, obj, alias):
result = u'D%s\n' % obj.date.strftime('%d/%m/%y')
result += u'T%s\n' % obj.amount
if hasattr(obj, 'category') and not empty(obj.category):
result += u'N%s\n' % obj.category
result += u'M%s\n' % obj.raw
result += u'^'
return result
class PrettyQifFormatter(QifFormatter):
MANDATORY_FIELDS = ('id', 'date', 'raw', 'amount', 'category')
def start_format(self, **kwargs):
self.output(u'!Type:Bank')
def format_obj(self, obj, alias):
if hasattr(obj, 'rdate') and not empty(obj.rdate):
result = u'D%s\n' % obj.rdate.strftime('%d/%m/%y')
else:
result = u'D%s\n' % obj.date.strftime('%d/%m/%y')
result += u'T%s\n' % obj.amount
if hasattr(obj, 'category') and not empty(obj.category):
result += u'N%s\n' % obj.category
if hasattr(obj, 'label') and not empty(obj.label):
result += u'M%s\n' % obj.label
else:
result += u'M%s\n' % obj.raw
result += u'^'
return result
class TransactionsFormatter(IFormatter):
MANDATORY_FIELDS = ('date', 'label', 'amount')
TYPES = ['', 'Transfer', 'Order', 'Check', 'Deposit', 'Payback', 'Withdrawal', 'Card', 'Loan', 'Bank']
def start_format(self, **kwargs):
self.output(' Date Category Label Amount ')
self.output('------------+------------+---------------------------------------------------+-----------')
def format_obj(self, obj, alias):
if hasattr(obj, 'category') and obj.category:
_type = obj.category
else:
try:
_type = self.TYPES[obj.type]
except (IndexError, AttributeError):
_type = ''
label = obj.label or ''
if not label and hasattr(obj, 'raw'):
label = obj.raw or ''
date = obj.date.strftime('%Y-%m-%d') if not empty(obj.date) else ''
amount = obj.amount or Decimal('0')
return ' %s %s %s %s' % (self.colored('%-10s' % date, 'blue'),
self.colored('%-12s' % _type[:12], 'magenta'),
self.colored('%-50s' % label[:50], 'yellow'),
self.colored('%10.2f' % amount, 'green' if amount >= 0 else 'red'))
class TransferFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'exec_date', 'account_label', 'recipient_label', 'amount')
DISPLAYED_FIELDS = ('label', 'account_iban', 'recipient_iban', 'currency')
def format_obj(self, obj, alias):
result = u'------- Transfer %s -------\n' % obj.fullid
result += u'Date: %s\n' % obj.exec_date
if obj.account_iban:
result += u'Origin: %s (%s)\n' % (obj.account_label, obj.account_iban)
else:
result += u'Origin: %s\n' % obj.account_label
if obj.recipient_iban:
result += u'Recipient: %s (%s)\n' % (obj.recipient_label, obj.recipient_iban)
else:
result += u'Recipient: %s\n' % obj.recipient_label
result += u'Amount: %.2f %s\n' % (obj.amount, obj.currency or '')
if obj.label:
result += u'Label: %s\n' % obj.label
return result
class InvestmentFormatter(IFormatter):
MANDATORY_FIELDS = ('label', 'quantity', 'unitvalue')
DISPLAYED_FIELDS = ('code', 'diff')
tot_valuation = Decimal(0)
tot_diff = Decimal(0)
def start_format(self, **kwargs):
self.output(' Label Code Quantity Unit Value Valuation diff ')
self.output('-------------------------------+--------------+------------+------------+------------+---------')
def check_emptyness(self, obj):
if not empty(obj):
return (obj, '%11.2f')
return ('---', '%11s')
def format_obj(self, obj, alias):
label = obj.label
if not empty(obj.diff):
diff = obj.diff
elif not empty(obj.quantity) and not empty(obj.unitprice):
diff = obj.valuation - (obj.quantity * obj.unitprice)
else:
diff = '---'
format_diff = '%8s'
if isinstance(diff, Decimal):
format_diff = '%8.2f'
self.tot_diff += diff
if not empty(obj.quantity):
quantity = obj.quantity
format_quantity = '%11.2f'
if obj.quantity == obj.quantity.to_integral():
format_quantity = '%11d'
else:
format_quantity = '%11s'
quantity = '---'
unitvalue, format_unitvalue = self.check_emptyness(obj.unitvalue)
valuation, format_valuation = self.check_emptyness(obj.valuation)
if isinstance(valuation, Decimal):
self.tot_valuation += obj.valuation
if empty(obj.code) and not empty(obj.description):
code = obj.description
else:
code = obj.code
return u' %s %s %s %s %s %s' % \
(self.colored('%-30s' % label[:30], 'red'),
self.colored('%-12s' % code[:12], 'yellow') if not empty(code) else ' ' * 12,
self.colored(format_quantity % quantity, 'yellow'),
self.colored(format_unitvalue % unitvalue, 'yellow'),
self.colored(format_valuation % valuation, 'yellow'),
self.colored(format_diff % diff, 'green' if not isinstance(diff, str) and diff >= 0 else 'red')
)
def flush(self):
self.output(u'-------------------------------+--------------+------------+------------+------------+---------')
self.output(u' Total %s %s' %
(self.colored('%11.2f' % self.tot_valuation, 'yellow'),
self.colored('%9.2f' % self.tot_diff, 'green' if self.tot_diff >= 0 else 'red'))
)
self.tot_valuation = Decimal(0)
self.tot_diff = Decimal(0)
class RecipientListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'label')
DISPLAYED_FIELDS = ('iban', 'bank_name')
def start_format(self, **kwargs):
self.output('Available recipients:')
def get_title(self, obj):
details = ' - '.join(filter(None, (obj.iban, obj.bank_name)))
if details:
return '%s (%s)' % (obj.label, details)
return obj.label
class AdvisorListFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name')
def start_format(self, **kwargs):
self.output(' Bank Name Contacts')
self.output('-----------------+------------------------------+-----------------------------------------')
def format_obj(self, obj, alias):
bank = obj.backend
phones = ""
contacts = []
if not empty(obj.phone):
phones += obj.phone
if not empty(obj.mobile):
if phones != "":
phones += " or %s" % obj.mobile
else:
phones += obj.mobile
if phones:
contacts.append(phones)
for attr in ('email', 'agency', 'address'):
value = getattr(obj, attr)
if not empty(value):
contacts.append(value)
if len(contacts) > 0:
first_contact = contacts.pop(0)
else:
first_contact = ""
result = u" %s %s %s " % (self.colored('%-15s' % bank, 'yellow'),
self.colored('%-30s' % obj.name, 'red'),
self.colored("%-30s" % first_contact, 'green'))
for contact in contacts:
result += "\n %s %s" % ((" ") * 47, self.colored("%-25s" % contact, 'green'))
return result
class AccountListFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'label', 'balance', 'coming', 'type')
totals = {}
def start_format(self, **kwargs):
self.output(' %s Account Balance Coming ' % ((' ' * 15) if not self.interactive else ''))
self.output('------------------------------------------%s+----------+----------' % (('-' * 15) if not self.interactive else ''))
def format_obj(self, obj, alias):
if alias is not None:
id = '%s (%s)' % (self.colored('%3s' % ('#' + alias), 'red', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
clean = '#%s (%s)' % (alias, obj.backend)
if len(clean) < 15:
id += (' ' * (15 - len(clean)))
else:
id = self.colored('%30s' % obj.fullid, 'red', 'bold')
balance = obj.balance or Decimal('0')
coming = obj.coming or Decimal('0')
currency = obj.currency or 'EUR'
result = u'%s %s %s %s' % (id,
self.colored('%-25s' % obj.label[:25], 'yellow' if obj.type != Account.TYPE_LOAN else 'blue'),
self.colored('%9.2f' % obj.balance, 'green' if balance >= 0 else 'red') if not empty(obj.balance) else ' ' * 9,
self.colored('%9.2f' % obj.coming, 'green' if coming >= 0 else 'red') if not empty(obj.coming) else '')
currency_totals = self.totals.setdefault(currency, {})
currency_totals.setdefault('balance', Decimal(0))
currency_totals.setdefault('coming', Decimal(0))
if obj.type != Account.TYPE_LOAN:
currency_totals['balance'] += balance
currency_totals['coming'] += coming
return result
def flush(self):
self.output(u'------------------------------------------%s+----------+----------' % (('-' * 15) if not self.interactive else ''))
for currency, currency_totals in sorted(self.totals.items(), key=lambda k_v: (k_v[1]['balance'], k_v[1]['coming'], k_v[0])):
balance = currency_totals['balance']
coming = currency_totals['coming']
self.output(u'%s Total (%s) %s %s' % (
(' ' * 15) if not self.interactive else '',
currency,
self.colored('%8.2f' % balance, 'green' if balance >= 0 else 'red'),
self.colored('%8.2f' % coming, 'green' if coming >= 0 else 'red'))
)
self.totals.clear()
class Boobank(ReplApplication):
APPNAME = 'boobank'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon, Christophe Benz'
CAPS = CapBank
DESCRIPTION = "Console application allowing to list your bank accounts and get their balance, " \
"display accounts history and coming bank operations, and transfer money from an account to " \
"another (if available)."
SHORT_DESCRIPTION = "manage bank accounts"
EXTRA_FORMATTERS = {'account_list': AccountListFormatter,
'recipient_list': RecipientListFormatter,
'transfer': TransferFormatter,
'qif': QifFormatter,
'pretty_qif': PrettyQifFormatter,
'ofx': OfxFormatter,
'ops_list': TransactionsFormatter,
'investment_list': InvestmentFormatter,
'advisor_list': AdvisorListFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'ls': 'account_list',
'list': 'account_list',
'recipients': 'recipient_list',
'transfer': 'transfer',
'history': 'ops_list',
'coming': 'ops_list',
'investment': 'investment_list',
'advisor': 'advisor_list',
}
COLLECTION_OBJECTS = (Account, Transaction, )
def bcall_error_handler(self, backend, error, backtrace):
if isinstance(error, TransferStep):
params = {}
for field in error.fields:
v = self.ask(field)
if v:
params[field.id] = v
#backend.config['accept_transfer'].set(v)
params['backends'] = backend
self.start_format()
for transfer in self.do('transfer', error.transfer, **params):
self.format(transfer)
elif isinstance(error, AddRecipientStep):
params = {}
params['backends'] = backend
for field in error.fields:
v = self.ask(field)
if v:
params[field.id] = v
next(iter(self.do('add_recipient', error.recipient, **params)))
else:
return ReplApplication.bcall_error_handler(self, backend, error, backtrace)
def load_default_backends(self):
self.load_backends(CapBank, storage=self.create_storage())
def _complete_account(self, exclude=None):
if exclude:
exclude = '%s@%s' % self.parse_id(exclude)
return [s for s in self._complete_object() if s != exclude]
def do_list(self, line):
"""
list [-U]
List accounts.
Use -U to disable sorting of results.
"""
return self.do_ls(line)
def show_history(self, command, line):
id, end_date = self.parse_command_args(line, 2, 1)
account = self.get_object(id, 'get_account', [])
if not account:
print('Error: account "%s" not found (Hint: try the command "list")' % id, file=self.stderr)
return 2
if end_date is not None:
try:
end_date = parse_date(end_date)
except ValueError:
print('"%s" is an incorrect date format (for example "%s")' %
(end_date, (datetime.date.today() - relativedelta(months=1)).strftime('%Y-%m-%d')), file=self.stderr)
return 3
old_count = self.options.count
self.options.count = None
self.start_format(account=account)
for transaction in self.do(command, account, backends=account.backend):
if end_date is not None and transaction.date < end_date:
break
self.format(transaction)
if end_date is not None:
self.options.count = old_count
def complete_history(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_account()
@defaultcount(10)
def do_history(self, line):
"""
history ID [END_DATE]
Display history of transactions.
If END_DATE is supplied, list all transactions until this date.
"""
return self.show_history('iter_history', line)
def complete_coming(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_account()
@defaultcount(10)
def do_coming(self, line):
"""
coming ID [END_DATE]
Display future transactions.
If END_DATE is supplied, show all transactions until this date.
"""
return self.show_history('iter_coming', line)
def complete_transfer(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_account()
if len(args) == 3:
return self._complete_account(args[1])
def do_add_recipient(self, line):
"""
add_recipient iban label
Add a recipient to a backend.
"""
iban, label = self.parse_command_args(line, 2, 2)
recipient = Recipient()
recipient.iban = iban
recipient.label = label
next(iter(self.do('add_recipient', recipient)))
def do_recipients(self, line):
"""
recipients ACCOUNT
List recipients of ACCOUNT
"""
id_from, = self.parse_command_args(line, 1, 1)
account = self.get_object(id_from, 'get_account', [])
if not account:
print('Error: account %s not found' % id_from, file=self.stderr)
return 1
self.objects = []
self.start_format()
for recipient in self.do('iter_transfer_recipients', account.id, backends=account.backend, caps=CapBankTransfer):
self.cached_format(recipient)
@contextmanager
def use_cmd_formatter(self, cmd):
self.set_formatter(self.commands_formatters.get(cmd, self.DEFAULT_FORMATTER))
try:
yield
finally:
self.flush()
def _build_transfer(self, line):
if self.interactive:
id_from, id_to, amount, reason = self.parse_command_args(line, 4, 0)
else:
id_from, id_to, amount, reason = self.parse_command_args(line, 4, 3)
missing = not bool(id_from and id_to and amount)
if id_from:
account = self.get_object(id_from, 'get_account', [])
id_from = account.id
if not account:
print('Error: account %s not found' % id_from, file=self.stderr)
return
else:
with self.use_cmd_formatter('list'):
self.do_ls('')
id_from = self.ask('Transfer money from account', default='')
if not id_from:
return
id_from, backend = self.parse_id(id_from)
account = find_object(self.objects, fullid='%s@%s' % (id_from, backend))
if not account:
return
id_from = account.id
if id_to:
id_to, backend_name_to = self.parse_id(id_to)
if account.backend != backend_name_to:
print("Transfer between different backends is not implemented", file=self.stderr)
return
rcpts = self.do('iter_transfer_recipients', id_from, backends=account.backend)
rcpt = find_object(rcpts, id=id_to)
else:
with self.use_cmd_formatter('recipients'):
self.do_recipients(account.fullid)
id_to = self.ask('Transfer money to recipient', default='')
if not id_to:
return
id_to, backend = self.parse_id(id_to)
rcpt = find_object(self.objects, fullid='%s@%s' % (id_to, backend))
if not rcpt:
return
if not amount:
amount = self.ask('Amount to transfer', default='', regexp=r'\d+(?:\.\d*)?')
try:
amount = Decimal(amount)
except (TypeError, ValueError, InvalidOperation):
print('Error: please give a decimal amount to transfer', file=self.stderr)
return
if amount <= 0:
print('Error: transfer amount must be strictly positive', file=self.stderr)
return
if missing:
reason = self.ask('Label of the transfer (seen by the recipient)', default='')
exec_date = datetime.date.today()
transfer = Transfer()
transfer.backend = account.backend
transfer.account_id = account.id
transfer.account_label = account.label
transfer.account_iban = account.iban
transfer.recipient_id = id_to
if rcpt:
# Try to find the recipient label. It can be missing from
# recipients list, for example for banks which allow transfers to
# arbitrary recipients.
transfer.recipient_label = rcpt.label
transfer.recipient_iban = rcpt.iban
transfer.amount = amount
transfer.label = reason or u''
transfer.exec_date = exec_date
return transfer
def do_transfer(self, line):
"""
transfer [ACCOUNT RECIPIENT AMOUNT [LABEL]]
Make a transfer beetwen two account
- ACCOUNT the source account
- RECIPIENT the recipient
- AMOUNT amount to transfer
- LABEL label of transfer
"""
transfer = self._build_transfer(line)
if transfer is None:
return 1
if self.interactive:
with self.use_cmd_formatter('transfer'):
self.start_format()
self.cached_format(transfer)
if not self.ask('Are you sure to do this transfer?', default=True):
return
# only keep basic fields because most modules don't handle others
transfer.account_label = None
transfer.account_iban = None
transfer.recipient_label = None
transfer.recipient_iban = None
self.start_format()
next(iter(self.do('transfer', transfer, backends=transfer.backend)))
def show_wealth(self, command, id):
account = self.get_object(id, 'get_account', [])
if not account:
print('Error: account "%s" not found (Hint: try the command "list")' % id, file=self.stderr)
return 2
self.start_format()
for el in self.do(command, account, backends=account.backend):
self.format(el)
def do_investment(self, id):
"""
investment ID
Display investments of an account.
"""
self.show_wealth('iter_investment', id)
def do_pocket(self, id):
"""
pocket ID
Display pockets of an account.
"""
self.show_wealth('iter_pocket', id)
def do_budgea(self, line):
"""
budgea USERNAME PASSWORD
Export your bank accounts and transactions to Budgea.
Budgea is an online web and mobile application to manage your bank
accounts. To avoid giving your credentials to this service, you can use
this command.
https://www.budgea.com
"""
username, password = self.parse_command_args(line, 2, 2)
client = APIBrowser(baseurl='https://budgea.biapi.pro/2.0/')
client.set_profile(Weboob(self.VERSION))
client.TIMEOUT = 60
try:
r = client.request('auth/token', data={'username': username, 'password': password, 'application': 'weboob'})
except BrowserHTTPError as r:
error = r.response.json()
print('Error: {}'.format(error.get('message', error['code'])), file=self.stderr)
return 1
client.session.headers['Authorization'] = 'Bearer %s' % r['token']
accounts = {}
for account in client.request('users/me/accounts')['accounts']:
if account['id_connection'] is None:
accounts[account['number']] = account
for account in self.do('iter_accounts'):
if account.id not in accounts:
r = client.request('users/me/accounts', data={'name': account.label,
'balance': account.balance,
'number': account.id,
})
self.logger.debug(r)
account_id = r['id']
else:
account_id = accounts[account.id]['id']
transactions = []
for tr in self.do('iter_history', account, backends=account.backend):
transactions.append({'original_wording': tr.raw,
'simplified_wording': tr.label,
'value': tr.amount,
'date': tr.date.strftime('%Y-%m-%d'),
})
r = client.request('users/me/accounts/%s/transactions' % account_id,
data={'transactions': transactions})
client.request('users/me/accounts/%s' % account_id, data={'balance': account.balance})
print('- %s (%s%s): %s new transactions' % (account.label, account.balance, account.currency_text, len(r)))
def do_advisor(self, line):
"""
advisor
Display advisors.
"""
self.start_format()
found = 0
for advisor in self.do('iter_contacts', caps=CapContact):
if isinstance(advisor, Advisor):
self.format(advisor)
found = 1
if not found:
print('Error: no advisor found', file=self.stderr)
def do_profile(self, line):
"""
profile
Display detailed information about person or company.
"""
self.start_format()
for profile in self.do('get_profile', caps=CapProfile):
self.format(profile)
def main(self, argv):
self.load_config()
return super(Boobank, self).main(argv)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobathon/ 0000775 0000000 0000000 00000000000 13204006733 0026236 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobathon/__init__.py 0000664 0000000 0000000 00000001426 13204006733 0030352 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobathon import Boobathon
__all__ = ['Boobathon']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobathon/boobathon.py 0000664 0000000 0000000 00000065767 13204006733 0030610 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import datetime, timedelta
import re
import sys
from random import choice
from collections import OrderedDict
from weboob.capabilities.content import CapContent
from weboob.tools.application.repl import ReplApplication
from weboob.tools.compat import urlsplit
__all__ = ['Boobathon']
class Task(object):
STATUS_NONE = 0
STATUS_PROGRESS = 1
STATUS_DONE = 2
def __init__(self, backend, capability):
self.backend = backend
self.capability = capability
self.status = self.STATUS_NONE
self.date = None
self.branch = u''
def __repr__(self):
return '' % (self.backend, self.capability)
class Member(object):
def __init__(self, id, name):
self.name = name
self.id = id
self.tasks = []
self.availabilities = u''
self.repository = None
self.hardware = u''
self.is_me = False
def shortname(self):
name = self.name
if len(name) > 20:
name = '%s..' % name[:18]
return name
class Event(object):
def __init__(self, name, backend):
self.my_id = backend.browser.get_userid()
self.name = 'wiki/weboob/%s' % name
self.description = None
self.date = None
self.begin = None
self.end = None
self.location = None
self.winner = None
self.backend = backend
self.members = OrderedDict()
self.load()
def get_me(self):
return self.members.get(self.backend.browser.get_userid(), None)
def currently_in_event(self):
if not self.date or not self.begin or not self.end:
return False
return self.begin < datetime.now() < self.end
def is_closed(self):
return self.end < datetime.now()
def format_duration(self):
if not self.begin or not self.end:
return None
delta = self.end - self.begin
return '%02d:%02d' % (delta.seconds/3600, delta.seconds%3600)
def check_time_coherence(self):
"""
Check if the end's day is before the begin's one, in
case it stops at the next day (eg. 15h->1h).
If it occures, add a day.
"""
if self.begin > self.end:
self.end = self.end + timedelta(1)
def load(self):
self.content = self.backend.get_content(self.name)
self.members.clear()
member = None
for line in self.content.content.split('\n'):
line = line.strip()
if line.startswith('h1. '):
self.title = line[4:]
elif line.startswith('h3=. '):
m = re.match('h3=. Event finished. Winner is "(.*)":/users/(\d+)\!', line)
if not m:
print('Unable to parse h3=: %s' % line, file=self.stderr)
continue
self.winner = Member(int(m.group(2)), m.group(1))
elif line.startswith('h2. '):
continue
elif line.startswith('h3. '):
m = re.match('h3. "(.*)":/users/(\d+)', line)
if not m:
print('Unable to parse user "%s"' % line, file=self.stderr)
continue
member = Member(int(m.group(2)), m.group(1))
if member.id == self.my_id:
member.is_me = True
if self.winner is not None and member.id == self.winner.id:
self.winner = member
self.members[member.id] = member
elif self.description is None and len(line) > 0 and line != '{{TOC}}':
self.description = line
elif line.startswith('* '):
m = re.match('\* \*(\w+)\*: (.*)', line)
if not m:
continue
key, value = m.group(1), m.group(2)
if member is None:
if key == 'Date':
self.date = self.parse_date(value)
elif key == 'Start' or key == 'Begin':
self.begin = self.parse_time(value)
elif key == 'End':
self.end = self.parse_time(value)
self.check_time_coherence()
elif key == 'Location':
self.location = value
else:
if key == 'Repository':
m = re.match('"(.*.git)":.*', value)
if m:
member.repository = m.group(1)
else:
member.repository = value
elif key == 'Hardware':
member.hardware = value
elif key == 'Availabilities':
member.availabilities = value
elif line.startswith('[['):
m = re.match('\[\[(\w+)\]\]\|\[\[(\w+)\]\]\|(.*)\|', line)
if not m:
print('Unable to parse task: "%s"' % line, file=self.stderr)
continue
task = Task(m.group(1), m.group(2))
member.tasks.append(task)
if m.group(3) == '!/img/weboob/_progress.png!':
task.status = task.STATUS_PROGRESS
continue
mm = re.match('!/img/weboob/_done.png! (\d+):(\d+) (\w+)', m.group(3))
if mm and self.date:
task.status = task.STATUS_DONE
task.date = datetime(self.date.year,
self.date.month,
self.date.day,
int(mm.group(1)),
int(mm.group(2)))
task.branch = mm.group(3)
def parse_date(self, value):
try:
return datetime.strptime(value, '%Y-%m-%d')
except ValueError:
return None
def parse_time(self, value):
m = re.match('(\d+):(\d+)', value)
if not m:
return
try:
return self.date.replace(hour=int(m.group(1)),
minute=int(m.group(2)))
except ValueError:
return None
def save(self, message):
if self.winner:
finished = u'\nh3=. Event finished. Winner is "%s":/users/%d!\n' % (self.winner.name,
self.winner.id)
else:
finished = u''
s = u"""h1. %s
{{TOC}}
%s
h2. Event
%s
* *Date*: %s
* *Begin*: %s
* *End*: %s
* *Duration*: %s
* *Location*: %s
h2. Attendees
""" % (self.title,
finished,
self.description,
self.date.strftime('%Y-%m-%d') if self.date else '_Unknown_',
self.begin.strftime('%H:%M') if self.begin else '_Unknown_',
self.end.strftime('%H:%M') if self.end else '_Unknown_',
self.format_duration() or '_Unknown_',
self.location or '_Unknown_')
for member in self.members.values():
if self.date:
availabilities = ''
else:
availabilities = '* *Availabilities*: %s' % member.availabilities
if member.repository is None:
repository = '_Unknown_'
elif member.repository.endswith('.git'):
repository = '"%s":git://git.symlink.me/pub/%s ("http":http://git.symlink.me/?p=%s;a=summary)'
repository = repository.replace('%s', member.repository)
else:
repository = member.repository
s += u"""h3. "%s":/users/%d
* *Repository*: %s
* *Hardware*: %s
%s
|_.Backend|_.Capabilities|_.Status|""" % (member.name,
member.id,
repository,
member.hardware,
availabilities)
for task in member.tasks:
if task.status == task.STATUS_DONE:
status = '!/img/weboob/_done.png! %02d:%02d %s' % (task.date.hour,
task.date.minute,
task.branch)
elif task.status == task.STATUS_PROGRESS:
status = '!/img/weboob/_progress.png!'
else:
status = ' '
s += u"""
|=.!/img/weboob/%s.png!:/projects/weboob/wiki/%s
[[%s]]|[[%s]]|%s|""" % (task.backend.lower(), task.backend, task.backend, task.capability, status)
s += '\n\n'
self.content.content = s
self.backend.push_content(self.content, message)
class Boobathon(ReplApplication):
APPNAME = 'boobathon'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2011-YEAR Romain Bignon'
DESCRIPTION = 'Console application to participate to a Boobathon.'
SHORT_DESCRIPTION = "participate in a Boobathon"
CAPS = CapContent
SYNOPSIS = 'Usage: %prog [-dqv] [-b backends] [-cnfs] boobathon\n'
SYNOPSIS += ' %prog [--help] [--version]'
radios = []
def __init__(self, *args, **kwargs):
super(Boobathon, self).__init__(*args, **kwargs)
def main(self, argv):
if len(argv) < 2:
print('Please give the name of the boobathon', file=self.stderr)
return 1
self.event = Event(argv[1], choice(list(self.weboob.backend_instances.values())))
if self.event.description is None:
if not self.ask("This event doesn't seem to exist. Do you want to create it?", default=True):
return 1
self.edit_event()
self.save_event('Event created')
return ReplApplication.main(self, [argv[0]])
def save_event(self, message):
if self.ask("Do you confirm your changes?", default=True):
self.event.save(message)
return True
return False
def edit_event(self):
self.event.title = self.ask('Enter a title', default=self.event.title)
self.event.description = self.ask('Enter a description', default=self.event.description)
self.event.date = self.ask('Enter a date (yyyy-mm-dd)',
default=self.event.date.strftime('%Y-%m-%d') if self.event.date else '',
regexp='^(\d{4}-\d{2}-\d{2})?$')
if self.event.date:
self.event.date = datetime.strptime(self.event.date, '%Y-%m-%d')
s = self.ask('Begin at (HH:MM)',
default=self.event.begin.strftime('%H:%M') if self.event.begin else '',
regexp='^(\d{2}:\d{2})?$')
if s:
h, m = s.split(':')
self.event.begin = self.event.date.replace(hour=int(h), minute=int(m))
s = self.ask('End at (HH:MM)',
default=self.event.end.strftime('%H:%M') if self.event.end else '',
regexp='^(\d{2}:\d{2})?$')
if s:
h, m = s.split(':')
self.event.end = self.event.date.replace(hour=int(h), minute=int(m))
self.event.check_time_coherence()
self.event.location = self.ask('Enter a location', default=self.event.location)
def edit_member(self, member):
if member.name is None:
firstname = self.ask('Enter your firstname')
lastname = self.ask('Enter your lastname')
member.name = '%s %s' % (firstname, lastname)
else:
member.name = self.ask('Enter your name', default=member.name)
if self.event.date is None:
member.availabilities = self.ask('Enter availabilities', default=member.availabilities)
member.repository = self.ask('Enter your repository (ex. romain/weboob.git)', default=member.repository)
member.hardware = self.ask('Enter your hardware', default=member.hardware)
def do_progress(self, line):
"""
progress
Display progress of members.
"""
self.event.load()
for member in self.event.members.values():
if member.is_me and member is self.event.winner:
status = '\o/ ->'
elif member.is_me:
status = ' ->'
elif member is self.event.winner:
status = ' \o/'
else:
status = ' '
s = u' %s%20s %s|' % (status, member.shortname(), self.BOLD)
for task in member.tasks:
if task.status == task.STATUS_DONE:
s += '##'
elif task.status == task.STATUS_PROGRESS:
s += u'=>'
else:
s += ' '
s += '|%s' % self.NC
print(s)
print('')
now = datetime.now()
if self.event.begin > now:
d = self.event.begin - now
msg = 'The event will start in %d days, %02d:%02d:%02d'
elif self.event.end < now:
d = now - self.event.end
msg = 'The event is finished since %d days, %02d:%02d:%02d'
else:
tot = (self.event.end - self.event.begin).seconds
cur = (datetime.now() - self.event.begin).seconds
pct = cur*20/tot
progress = ''
for i in range(20):
if i < pct:
progress += '='
elif i == pct:
progress += '>'
else:
progress += ' '
print('Event started: %s |%s| %s' % (self.event.begin.strftime('%H:%M'),
progress,
self.event.end.strftime('%H:%M')))
d = self.event.end - now
msg = 'The event will be finished in %d days, %02d:%02d:%02d'
print(msg % (d.days, d.seconds/3600, d.seconds%3600/60, d.seconds%60))
def do_tasks(self, line):
"""
tasks
Display all tasks of members.
"""
self.event.load()
stop = False
i = -2
while not stop:
if i >= 0 and not i%2:
self.stdout.write(' #%-2d' % (i/2))
else:
self.stdout.write(' ')
if i >= 0 and i%2:
# second line of task, see if we'll stop
stop = True
for mem in self.event.members.values():
if len(mem.tasks) > (i/2+1):
# there are more tasks, don't stop now
stop = False
if i == -2:
self.stdout.write(' %s%-20s%s' % (self.BOLD, mem.shortname().encode('utf-8'), self.NC))
elif i == -1:
self.stdout.write(' %s%-20s%s' % (self.BOLD, '-' * len(mem.shortname()), self.NC))
elif len(mem.tasks) <= (i/2):
self.stdout.write(' ' * (20+1))
else:
task = mem.tasks[i/2]
if task.status == task.STATUS_DONE:
status = u'#'
elif task.status == task.STATUS_PROGRESS:
if not i%2:
status = u'|' #1st line
else:
status = u'v' #2nd line
else:
status = u' '
if not i%2: #1st line
line = u'%s %s' % (status, task.backend)
else: #2nd line
line = u'%s `-%s' % (status, task.capability[3:])
self.stdout.write((u' %-20s' % line).encode('utf-8'))
self.stdout.write('\n')
i += 1
def complete_close(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
self.event.load()
return [member.name for member in self.event.members.values()]
def do_close(self, name):
"""
close WINNER
Close the event and set the winner.
"""
self.event.load()
for member in self.event.members.values():
if member.name == name:
self.event.winner = member
if self.save_event('Close event'):
print('Event is now closed. Winner is %s!' % self.event.winner.name)
return
print('"%s" not found' % name, file=self.stderr)
return 3
def complete_edit(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return ['event', 'me']
def do_edit(self, line):
"""
edit [event | me]
Edit information about you or about event.
"""
if not line:
print('Syntax: edit [event | me]', file=self.stderr)
return 2
self.event.load()
if line == 'event':
self.edit_event()
self.save_event('Event edited')
elif line == 'me':
mem = self.event.get_me()
if not mem:
print('You haven\'t joined the event.', file=self.stderr)
return 1
self.edit_member(mem)
self.save_event('Member edited')
else:
print('Unable to edit "%s"' % line, file=self.stderr)
return 1
def do_info(self, line):
"""
info
Display information about this event.
"""
self.event.load()
print(self.event.title)
print('-' * len(self.event.title))
print(self.event.description)
print('')
print('Date:', self.event.date.strftime('%Y-%m-%d') if self.event.date else 'Unknown')
print('Begin:', self.event.begin.strftime('%H:%M') if self.event.begin else 'Unknown')
print('End:', self.event.end.strftime('%H:%M') if self.event.end else 'Unknown')
print('Duration:', self.event.format_duration() or 'Unknown')
print('Location:', self.event.location or 'Unknown')
print('')
print('There are %d members, use the "members" command to list them' % len(self.event.members))
if self.event.get_me() is None:
print('To join this event, use the command "join".')
def do_members(self, line):
"""
members
Display members information.
"""
self.event.load()
for member in self.event.members.values():
print(member.name)
print('-' * len(member.name))
print('Repository:', member.repository)
if self.event.date is None:
print('Availabilities:', member.availabilities)
print('Hardware:', member.hardware)
accompl = 0
for task in member.tasks:
if task.status == task.STATUS_DONE:
accompl += 1
print('%d tasks (%d accomplished)' % (len(member.tasks), accompl))
if member is self.event.winner:
print('=== %s is the winner!' % member.name)
print('')
print('Use the "tasks" command to display all tasks')
def do_join(self, line):
"""
join
Join this event.
"""
self.event.load()
if self.event.backend.browser.get_userid() in self.event.members:
print('You have already joined this event.', file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
m = Member(self.event.backend.browser.get_userid(), None)
self.edit_member(m)
self.event.members[m.id] = m
self.save_event('Joined the event')
def do_leave(self, line):
"""
leave
Leave this event.
"""
self.event.load()
if self.event.currently_in_event():
print('Unable to leave during the event, loser!', file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
try:
self.event.members.pop(self.event.backend.browser.get_userid())
except KeyError:
print("You have not joined this event.", file=self.stderr)
return 1
else:
self.save_event('Left the event')
def do_remtask(self, line):
"""
remtask TASK_ID
Remove a task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
try:
task_id = int(line)
except ValueError:
print('The task ID should be a number', file=self.stderr)
return 2
try:
task = mem.tasks.pop(task_id)
except IndexError:
print('Unable to find task #%d' % task_id, file=self.stderr)
return 1
else:
print('Removing task #%d (%s,%s).' % (task_id, task.backend, task.capability))
self.save_event('Remove task')
def do_addtask(self, line):
"""
addtask BACKEND CAPABILITY
Add a new task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
backend, capability = self.parse_command_args(line, 2, 2)
if not backend[0].isupper():
print('The backend name "%s" needs to start with a capital.' % backend, file=self.stderr)
return 2
if not capability.startswith('Cap') or not capability[3].isupper():
print('"%s" is not a proper capability name (must start with Cap).' % capability, file=self.stderr)
return 2
for task in mem.tasks:
if (task.backend,task.capability) == (backend,capability):
print("A task already exists for that.", file=self.stderr)
return 1
task = Task(backend, capability)
mem.tasks.append(task)
self.save_event('New task')
def do_start(self, line):
"""
start [TASK_ID]
Start a task. If you don't give a task ID, the first available
task will be taken.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print("You have not joined this event.", file=self.stderr)
return 1
if len(mem.tasks) == 0:
print("You don't have any task to do.", file=self.stderr)
return 1
if not self.event.currently_in_event():
print("You can't start a task, we are not in event.", file=self.stderr)
return 1
if line.isdigit():
task_id = int(line)
else:
task_id = -1
last_done = -1
for i, task in enumerate(mem.tasks):
if task.status == task.STATUS_DONE:
last_done = i
elif task.status == task.STATUS_PROGRESS:
task.status = task.STATUS_NONE
print('Task #%s (%s,%s) canceled.' % (i, task.backend, task.capability))
if (i == task_id or task_id < 0) and task.status == task.STATUS_NONE:
break
else:
print('Task not found.', file=self.stderr)
return 3
if task.status == task.STATUS_DONE:
print('Task is already done.', file=self.stderr)
return 1
task.status = task.STATUS_PROGRESS
mem.tasks.remove(task)
mem.tasks.insert(last_done + 1, task)
self.save_event('Started a task')
def do_done(self, line):
"""
done
Set the current task as done.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
for i, task in enumerate(mem.tasks):
if task.status == task.STATUS_PROGRESS:
print('Task (%s,%s) done! (%d%%)' % (task.backend, task.capability, (i+1)*100/len(mem.tasks)))
if self.event.currently_in_event():
task.status = task.STATUS_DONE
task.date = datetime.now()
task.branch = self.ask('Enter name of branch')
self.save_event('Task accomplished')
else:
task.status = task.STATUS_NONE
print('Oops, you are out of event. Canceling the task...', file=self.stderr)
self.save_event('Cancel task')
return 1
return
print("There isn't any task in progress.", file=self.stderr)
return 1
def do_cancel(self, line):
"""
cancel
Cancel the current task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
for task in mem.tasks:
if task.status == task.STATUS_PROGRESS:
print('Task (%s,%s) canceled.' % (task.backend, task.capability))
task.status = task.STATUS_NONE
self.save_event('Cancel task')
return
print("There isn't any task in progress.", file=self.stderr)
return 1
def load_default_backends(self):
"""
Overload a Application method.
"""
for backend_name, module_name, params in self.weboob.backends_config.iter_backends():
if module_name != 'redmine':
continue
v = urlsplit(params['url'])
if v.netloc == 'symlink.me':
self.load_backends(names=[backend_name])
return
if not self.check_loaded_backends({'url': 'https://symlink.me'}):
print("Ok, so leave now, fag.")
sys.exit(0)
def is_module_loadable(self, module):
"""
Overload a ConsoleApplication method.
"""
return module.name == 'redmine'
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobcoming/ 0000775 0000000 0000000 00000000000 13204006733 0026401 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobcoming/__init__.py 0000664 0000000 0000000 00000001423 13204006733 0030512 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobcoming import Boobcoming
__all__ = ['Boobcoming']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobcoming/boobcoming.py 0000664 0000000 0000000 00000040273 13204006733 0031077 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import time, datetime
from dateutil import tz
from weboob.tools.date import parse_date
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.capabilities.base import empty
from weboob.capabilities.calendar import CapCalendarEvent, Query, CATEGORIES, BaseCalendarEvent, TICKET, STATUS
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.config.yamlconfig import YamlConfig
__all__ = ['Boobcoming']
class UpcomingSimpleFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'category', 'summary', 'status')
def format_obj(self, obj, alias):
result = u'%s - %s' % (obj.backend, obj.category)
if not empty(obj.start_date):
result += u' - %s' % obj.start_date.strftime('%H:%M')
result += u' - %s' % obj.summary
if obj.status == STATUS.CANCELLED:
result += u' (cancelled)'
return result
class ICalFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'status')
def start_format(self, **kwargs):
result = u'BEGIN:VCALENDAR\r\n'
result += u'VERSION:2.0\r\n'
result += u'PRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\n'
self.output(result)
def format_obj(self, obj, alias):
result = u'BEGIN:VEVENT\r\n'
utc_zone = tz.gettz('UTC')
event_timezone = tz.gettz(obj.timezone)
start_date = obj.start_date if not empty(obj.start_date) else datetime.now()
start_date = start_date.replace(tzinfo=event_timezone)
utc_start_date = start_date.astimezone(utc_zone)
result += u'DTSTART:%s\r\n' % utc_start_date.strftime("%Y%m%dT%H%M%SZ")
end_date = obj.end_date if not empty(obj.end_date) else datetime.combine(start_date, time.max)
end_date = end_date.replace(tzinfo=event_timezone)
utc_end_date = end_date.astimezone(utc_zone)
result += u'DTEND:%s\r\n' % utc_end_date.strftime("%Y%m%dT%H%M%SZ")
result += u'SUMMARY:%s\r\n' % obj.summary
result += u'UID:%s\r\n' % obj.id
result += u'STATUS:%s\r\n' % obj.status
location = ''
if hasattr(obj, 'location') and not empty(obj.location):
location += obj.location + ' '
if hasattr(obj, 'city') and not empty(obj.city):
location += obj.city + ' '
if not empty(location):
result += u'LOCATION:%s\r\n' % location
if hasattr(obj, 'categories') and not empty(obj.categories):
result += u'CATEGORIES:%s\r\n' % obj.categories
if hasattr(obj, 'description') and not empty(obj.description):
result += u'DESCRIPTION:%s\r\n' % obj.description.strip(' \t\n\r')\
.replace('\r', '')\
.replace('\n', r'\n')\
.replace(',', '\,')
if hasattr(obj, 'transp') and not empty(obj.transp):
result += u'TRANSP:%s\r\n' % obj.transp
if hasattr(obj, 'sequence') and not empty(obj.sequence):
result += u'SEQUENCE:%s\r\n' % obj.sequence
if hasattr(obj, 'url') and not empty(obj.url):
result += u'URL:%s\r\n' % obj.url
result += u'END:VEVENT\r\n'
return result
def flush(self, **kwargs):
self.output(u'END:VCALENDAR')
class UpcomingListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'category', 'status')
def get_title(self, obj):
return ' %s - %s ' % (obj.category, obj.summary)
def get_description(self, obj):
result = u''
if not empty(obj.start_date):
result += u'\tDate: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'\tHour: %s' % obj.start_date.strftime('%H:%M')
if not empty(obj.end_date):
result += ' - %s' % obj.end_date.strftime('%H:%M')
days_diff = (obj.end_date - obj.start_date).days
if days_diff >= 1:
result += ' (%i day(s) later)' % (days_diff)
result += '\n'
if obj.status == STATUS.CANCELLED:
result += '\tStatus: Cancelled\n'
return result.strip('\n\t')
class UpcomingFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'category', 'status')
def format_obj(self, obj, alias):
result = u'%s%s - %s%s\n' % (self.BOLD, obj.category, obj.summary, self.NC)
if not empty(obj.start_date):
if not empty(obj.end_date):
days_diff = (obj.end_date - obj.start_date).days
if days_diff >= 1:
result += u'From: %s to %s ' % (obj.start_date.strftime('%A %d %B %Y'),
obj.end_date.strftime('%A %d %B %Y'))
else:
result += u'Date: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'Hour: %s' % obj.start_date.strftime('%H:%M')
result += ' - %s' % obj.end_date.strftime('%H:%M')
else:
result += u'Date: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'Hour: %s' % obj.start_date.strftime('%H:%M')
result += '\n'
if hasattr(obj, 'location') and not empty(obj.location):
result += u'Location: %s\n' % obj.location
if hasattr(obj, 'city') and not empty(obj.city):
result += u'City: %s\n' % obj.city
if hasattr(obj, 'event_planner') and not empty(obj.event_planner):
result += u'Event planner: %s\n' % obj.event_planner
if hasattr(obj, 'booked_entries') and not empty(obj.booked_entries) and \
hasattr(obj, 'max_entries') and not empty(obj.max_entries):
result += u'Entry: %s/%s \n' % (obj.booked_entries, obj.max_entries)
elif hasattr(obj, 'booked_entries') and not empty(obj.booked_entries):
result += u'Entry: %s \n' % (obj.booked_entries)
elif hasattr(obj, 'max_entries') and not empty(obj.max_entries):
result += u'Max entries: %s \n' % (obj.max_entries)
if hasattr(obj, 'description') and not empty(obj.description):
result += u'Description:\n %s\n\n' % obj.description
if hasattr(obj, 'price') and not empty(obj.price):
result += u'Price: %.2f\n' % obj.price
if hasattr(obj, 'ticket') and not empty(obj.ticket):
result += u'Ticket: %s\n' % obj.ticket
if hasattr(obj, 'url') and not empty(obj.url):
result += u'URL: %s\n' % obj.url
if hasattr(obj, 'status') and not empty(obj.status):
result += u'Status: %s\n' % obj.status
return result
class Boobcoming(ReplApplication):
APPNAME = 'boobcoming'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Bezleputh'
DESCRIPTION = "Console application to see upcoming events."
SHORT_DESCRIPTION = "see upcoming events"
CAPS = CapCalendarEvent
EXTRA_FORMATTERS = {'upcoming_list': UpcomingListFormatter,
'upcoming': UpcomingFormatter,
'simple_upcoming': UpcomingSimpleFormatter,
'ical_formatter': ICalFormatter,
}
COMMANDS_FORMATTERS = {'list': 'upcoming_list',
'search': 'upcoming_list',
'load': 'upcoming_list',
'ls': 'upcoming_list',
'info': 'upcoming',
'export': 'ical_formatter'
}
def main(self, argv):
self.load_config(klass=YamlConfig)
return ReplApplication.main(self, argv)
def comp_object(self, obj1, obj2):
if isinstance(obj1, BaseCalendarEvent) and isinstance(obj2, BaseCalendarEvent):
if obj1.start_date == obj2.start_date:
return 0
if obj1.start_date > obj2.start_date:
return 1
return -1
else:
return super(Boobcoming, self).comp_object(obj1, obj2)
def select_values(self, values_from, values_to, query_str):
r = 'notempty'
while r != '':
for value in values_from.values:
print(' %s%2d)%s [%s] %s' % (self.BOLD,
values_from.index[value] + 1,
self.NC,
'x' if value in values_to else ' ',
value))
r = self.ask(query_str, regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(values_from.values):
continue
value = values_from.values[r - 1]
if value in values_to:
values_to.remove(value)
else:
values_to.append(value)
@defaultcount(10)
def do_search(self, line):
"""
search
search for an event. Parameters interactively asked
"""
query = Query()
self.select_values(CATEGORIES, query.categories, ' Select category (or empty to stop)')
self.select_values(TICKET, query.ticket, ' Select tickets status (or empty to stop)')
if query.categories and len(query.categories) > 0 and query.ticket and len(query.ticket) > 0:
query.city = self.ask('Enter a city', default='')
query.summary = self.ask('Enter a title', default='')
start_date = self.ask_date('Enter a start date', default='today')
end_date = self.ask_date('Enter a end date', default='')
if end_date:
if end_date == start_date:
end_date = datetime.combine(start_date, time.max)
else:
end_date = datetime.combine(end_date, time.max)
query.start_date = datetime.combine(start_date, time.min)
query.end_date = end_date
save_query = self.ask('Save query (y/n)', default='n')
if save_query.upper() == 'Y':
name = ''
while not name:
name = self.ask('Query name')
self.config.set('queries', name, query)
self.config.save()
self.complete_search(query)
def complete_search(self, query):
self.change_path([u'events'])
self.start_format()
for event in self.do('search_events', query):
if event:
self.cached_format(event)
def ask_date(self, txt, default=''):
r = self.ask(txt, default=default)
return parse_date(r)
@defaultcount(10)
def do_load(self, query_name):
"""
load [query name]
without query name : list loadable queries
with query name laod query
"""
queries = self.config.get('queries')
if not queries:
print('There is no saved queries', file=self.stderr)
return 2
if not query_name:
for name in queries.keys():
print(' %s* %s %s' % (self.BOLD,
self.NC,
name))
query_name = self.ask('Which one')
if query_name in queries:
self.complete_search(queries.get(query_name))
else:
print('Unknown query', file=self.stderr)
return 2
@defaultcount(10)
def do_list(self, line):
"""
list [PATTERN]
List upcoming events, pattern can be an english or french week day, 'today' or a date (dd/mm/yy[yy])
"""
self.change_path([u'events'])
if line:
_date = parse_date(line)
if not _date:
print('Invalid argument: %s' % self.get_command_help('list'), file=self.stderr)
return 2
date_from = datetime.combine(_date, time.min)
date_to = datetime.combine(_date, time.max)
else:
date_from = datetime.now()
date_to = None
for event in self.do('list_events', date_from, date_to):
self.cached_format(event)
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about an event.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
event = self.get_object(_id, 'get_event')
if not event:
print('Upcoming event not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(event)
def do_export(self, line):
"""
export FILENAME [ID1 ID2 ID3 ...]
ID is the identifier of the event. If no ID every events are exported
FILENAME is where to write the file. If FILENAME is '-', the file is written to stdout.
Export event in ICALENDAR format
"""
if not line:
print('This command takes at leat one argument: %s' % self.get_command_help('export'), file=self.stderr)
return 2
_file, args = self.parse_command_args(line, 2, req_n=1)
l = self.retrieve_events(args)
if l == 3:
return 3
if not _file == "-":
dest = self.check_file_ext(_file)
self.formatter.outfile = dest
self.formatter.start_format()
for item in l:
self.format(item)
def retrieve_events(self, args):
l = []
if not args:
_ids = []
for event in self.do('list_events', datetime.now(), None):
_ids.append(event.id)
else:
_ids = args.strip().split(' ')
for _id in _ids:
event = self.get_object(_id, 'get_event')
if not event:
print('Upcoming event not found: %s' % _id, file=self.stderr)
return 3
l.append(event)
return l
def check_file_ext(self, _file):
splitted_file = _file.split('.')
if splitted_file[-1] != 'ics':
return "%s.ics" % _file
else:
return _file
def do_attends(self, line):
"""
attends ID1 [ID2 ID3 ...]
Register as participant of an event.
ID is the identifier of the event.
"""
if not line:
print('This command takes at leat one argument: %s' % self.get_command_help('attends'), file=self.stderr)
return 2
args = self.parse_command_args(line, 1, req_n=1)
l = self.retrieve_events(args[0])
for event in l:
# we wait till the work be done, else the errors are not handled
self.do('attends_event', event, True).wait()
def do_unattends(self, line):
"""
unattends ID1 [ID2 ID3 ...]
Unregister you participation for an event.
ID is the identifier of the event.
"""
if not line:
print('This command takes at leat one argument: %s' % self.get_command_help('unattends'), file=self.stderr)
return 2
args = self.parse_command_args(line, 1, req_n=1)
l = self.retrieve_events(args[0])
for event in l:
self.do('attends_event', event, False)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobill/ 0000775 0000000 0000000 00000000000 13204006733 0025705 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobill/__init__.py 0000664 0000000 0000000 00000001424 13204006733 0030017 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobill import Boobill
__all__ = ['Boobill']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobill/boobill.py 0000664 0000000 0000000 00000021742 13204006733 0027707 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-2013 Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from decimal import Decimal
from weboob.capabilities.bill import CapDocument, Detail, Subscription
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import PrettyFormatter
from weboob.tools.application.base import MoreResultsAvailable
from weboob.core import CallErrors
__all__ = ['Boobill']
class SubscriptionsFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'label')
def get_title(self, obj):
if obj.renewdate:
return u"%s - %s" % (obj.label, obj.renewdate.strftime('%d/%m/%y'))
return obj.label
class Boobill(ReplApplication):
APPNAME = 'boobill'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Florent Fourcot'
DESCRIPTION = 'Console application allowing to get/download documents and bills.'
SHORT_DESCRIPTION = "get/download documents and bills"
CAPS = CapDocument
COLLECTION_OBJECTS = (Subscription, )
EXTRA_FORMATTERS = {'subscriptions': SubscriptionsFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'subscriptions': 'subscriptions',
'ls': 'subscriptions',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def exec_method(self, id, method):
l = []
id, backend_name = self.parse_id(id)
if not id:
for subscrib in self.get_object_list('iter_subscription'):
l.append((subscrib.id, subscrib.backend))
else:
l.append((id, backend_name))
more_results = []
not_implemented = []
self.start_format()
for id, backend in l:
names = (backend,) if backend is not None else None
try:
for result in self.do(method, id, backends=names):
self.format(result)
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MoreResultsAvailable):
more_results.append(id + u'@' + backend.name)
elif isinstance(error, NotImplementedError):
if backend not in not_implemented:
not_implemented.append(backend)
else:
self.bcall_error_handler(backend, error, backtrace)
if len(more_results) > 0:
print('Hint: There are more results available for %s (use option -n or count command)' % (', '.join(more_results)), file=self.stderr)
for backend in not_implemented:
print(u'Error(%s): This feature is not supported yet by this backend.' % backend.name, file=self.stderr)
def do_subscriptions(self, line):
"""
subscriptions
List all subscriptions.
"""
return self.do_ls(line)
def do_details(self, id):
"""
details [ID]
Get details of subscriptions.
If no ID given, display all details of all backends.
"""
l = []
id, backend_name = self.parse_id(id)
if not id:
for subscrib in self.get_object_list('iter_subscription'):
l.append((subscrib.id, subscrib.backend))
else:
l.append((id, backend_name))
for id, backend in l:
names = (backend,) if backend is not None else None
# XXX: should be generated by backend? -Flo
# XXX: no, but you should do it in a specific formatter -romain
# TODO: do it, and use exec_method here. Code is obsolete
mysum = Detail()
mysum.label = u"Sum"
mysum.infos = u"Generated by boobill"
mysum.price = Decimal("0.")
self.start_format()
for detail in self.do('get_details', id, backends=names):
self.format(detail)
mysum.price = detail.price + mysum.price
self.format(mysum)
def do_balance(self, id):
"""
balance [ID]
Get balance of subscriptions.
If no ID given, display balance of all backends.
"""
self.exec_method(id, 'get_balance')
@defaultcount(10)
def do_history(self, id):
"""
history [ID]
Get the history of subscriptions.
If no ID given, display histories of all backends.
"""
self.exec_method(id, 'iter_bills_history')
@defaultcount(10)
def do_documents(self, id):
"""
documents [ID]
Get the list of documents for subscriptions.
If no ID given, display documents of all backends
"""
self.exec_method(id, 'iter_documents')
@defaultcount(10)
def do_bills(self, id):
"""
bills [ID]
Get the list of bills documents for subscriptions.
If no ID given, display bills of all backends
"""
self.exec_method(id, 'iter_bills')
def do_download(self, line, force_pdf=False):
"""
download [ID | all] [FILENAME]
download ID [FILENAME]
download the document
id is the identifier of the document (hint: try documents command)
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
download all [ID]
You can use special word "all" and download all documents of
subscription identified by ID.
If Id not given, download documents of all subscriptions.
"""
id, dest = self.parse_command_args(line, 2, 1)
id, backend_name = self.parse_id(id)
if not id:
print('Error: please give a document ID (hint: use documents command)', file=self.stderr)
return 2
names = (backend_name,) if backend_name is not None else None
# Special keywords, download all documents of all subscriptions
if id == "all":
callback = self.download_all if not force_pdf else self.download_all_pdf
if dest is None:
for subscription in self.do('iter_subscription', backends=names):
callback(subscription.id, names)
return
else:
callback(dest, names)
return
if dest is None:
for document in self.do('get_document', id, backends=names):
dest = id + "." + (document.format if not force_pdf else 'pdf')
if 'document' not in locals():
document = id
for buf in self.do('download_document' if not force_pdf else 'download_document_pdf', document, backends=names):
if buf:
if dest == "-":
print(buf)
else:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write document in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
def do_download_pdf(self, line):
"""
download_pdf [id | all]
download function with forced PDF conversion.
"""
return self.do_download(line, force_pdf=True)
def download_all(self, id, names, force_pdf=False):
id, backend_name = self.parse_id(id)
for document in self.do('iter_documents', id, backends=names):
dest = document.id + "." + (document.format if not force_pdf else 'pdf')
for buf in self.do('download_document' if not force_pdf else 'download_document_pdf', document, backends=names):
if buf:
if dest == "-":
print(buf)
else:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write bill in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
def download_all_pdf(self, id, names):
"""
download_pdf all
download_all function with forced PDF conversion.
"""
return self.download_all(id, names, force_pdf=True)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/booblyrics/ 0000775 0000000 0000000 00000000000 13204006733 0026432 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/booblyrics/__init__.py 0000664 0000000 0000000 00000001432 13204006733 0030543 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .booblyrics import Booblyrics
__all__ = ['Booblyrics']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/booblyrics/booblyrics.py 0000664 0000000 0000000 00000007037 13204006733 0031162 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.lyrics import CapLyrics
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Booblyrics', 'LyricsGetFormatter', 'LyricsListFormatter']
class LyricsGetFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'title', 'artist', 'content')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'ID: %s\n' % obj.fullid
result += 'Title: %s\n' % obj.title
result += 'Artist: %s\n' % obj.artist
result += '\n%sContent%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.content
return result
class LyricsListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'artist')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
artist = u''
if not empty(obj.artist):
artist = obj.artist
return '%s' % artist
class Booblyrics(ReplApplication):
APPNAME = 'booblyrics'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Julien Veyssier'
DESCRIPTION = "Console application allowing to search for song lyrics on various websites."
SHORT_DESCRIPTION = "search and display song lyrics"
CAPS = CapLyrics
EXTRA_FORMATTERS = {'lyrics_list': LyricsListFormatter,
'lyrics_get': LyricsGetFormatter,
}
COMMANDS_FORMATTERS = {'search': 'lyrics_list',
'get': 'lyrics_get',
}
SEARCH_CRITERIAS = ['artist', 'song']
def complete_get(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_get(self, id):
"""
get ID
Display lyrics of the song.
"""
songlyrics = self.get_object(id, 'get_lyrics')
if not songlyrics:
print('Song lyrics not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(songlyrics)
def complete_search(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self.SEARCH_CRITERIAS
@defaultcount(10)
def do_search(self, line):
"""
search [artist | song] [PATTERN]
Search lyrics by artist name or by song title.
"""
criteria, pattern = self.parse_command_args(line, 2, 2)
self.change_path([u'search'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for songlyrics in self.do('iter_lyrics', criteria, pattern):
self.cached_format(songlyrics)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobmsg/ 0000775 0000000 0000000 00000000000 13204006733 0025713 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobmsg/__init__.py 0000664 0000000 0000000 00000001426 13204006733 0030027 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobmsg import Boobmsg
__all__ = ['Boobmsg']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobmsg/boobmsg.py 0000664 0000000 0000000 00000044342 13204006733 0027724 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import datetime
import hashlib
from tempfile import NamedTemporaryFile
from lxml import etree
from weboob.core import CallErrors
from weboob.capabilities.base import empty
from weboob.capabilities.messages import CapMessages, Message, Thread
from weboob.capabilities.account import CapAccount
from weboob.capabilities.contact import CapContact
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter
from weboob.tools.html import html2text
__all__ = ['Boobmsg']
class AtomFormatter(IFormatter):
MANDATORY_FIELDS = ('title', 'date', 'sender', 'content')
def start_format(self, **kwargs):
self.output(u'\n')
self.output(u'Atom feed by Weboob') # TODO : get backend name
self.output(u'%s' % datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"))
m = hashlib.md5()
m.update(datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"))
self.output(u'urn:md5:%s' % m.hexdigest())
def format_obj(self, obj, alias):
elem = etree.Element('entry')
title = etree.Element('title')
title.text = obj.title
elem.append(title)
id = etree.Element('id')
m = hashlib.md5()
m.update(obj.content.encode('utf8', 'ascii'))
id.text = "urn:md5:%s" % m.hexdigest()
elem.append(id)
link = etree.Element('link')
link.attrib["href"] = obj.thread.id
link.attrib["title"] = obj.title
link.attrib["type"] = "text/html"
elem.append(link)
author = etree.Element('author')
name = etree.Element('name')
if obj.sender:
name.text = obj.sender
else:
name.text = obj.backend
author.append(name)
elem.append(author)
date = etree.Element('updated')
date.text = obj.date.strftime("%Y-%m-%dT%H:%M:%SZ")
elem.append(date)
content = etree.Element('content')
content.text = obj.content
content.attrib["type"] = "html"
elem.append(content)
return etree.tostring(elem, pretty_print=True)
def flush(self):
self.output(u'')
class XHtmlFormatter(IFormatter):
MANDATORY_FIELDS = ('title', 'date', 'sender', 'signature', 'content')
def format_obj(self, obj, alias):
result = "
\n"
result += "
%s
" % (obj.title)
result += "
"
result += "
Date
%s
" % (obj.date.strftime('%Y-%m-%d %H:%M'))
result += "
Sender
%s
" % (obj.sender)
result += "
Signature
%s
" % (obj.signature)
result += "
"
result += "
%s
" % (obj.content)
result += "
\n"
return result
class MessageFormatter(IFormatter):
MANDATORY_FIELDS = ('title', 'date', 'sender', 'signature', 'content')
def format_obj(self, obj, alias):
result = u'%sTitle:%s %s\n' % (self.BOLD,
self.NC, obj.title)
result += u'%sDate:%s %s\n' % (self.BOLD,
self.NC, obj.date.strftime('%Y-%m-%d %H:%M'))
result += u'%sFrom:%s %s\n' % (self.BOLD,
self.NC, obj.sender)
if hasattr(obj, 'receivers') and obj.receivers:
result += u'%sTo:%s %s\n' % (self.BOLD,
self.NC,
', '.join(obj.receivers))
if obj.flags & Message.IS_HTML:
content = html2text(obj.content)
else:
content = obj.content
result += '\n%s' % content
if obj.signature:
if obj.flags & Message.IS_HTML:
signature = html2text(obj.signature)
else:
signature = obj.signature
result += '\n-- \n%s' % signature
return result
class MessagesListFormatter(IFormatter):
MANDATORY_FIELDS = ()
count = 0
_list_messages = False
def flush(self):
self.count = 0
def format_obj(self, obj, alias):
if not self._list_messages:
return self.format_dict_thread(obj, alias)
else:
return self.format_dict_messages(obj, alias)
def format_dict_thread(self, obj, alias):
self.count += 1
if self.interactive:
result = u'%s* (%d) %s (%s)%s' % (self.BOLD,
self.count,
obj.title, obj.backend,
self.NC)
else:
result = u'%s* (%s) %s%s' % (self.BOLD, obj.id,
obj.title,
self.NC)
if obj.date:
result += u'\n %s' % obj.date
return result
def format_dict_messages(self, obj, alias):
if obj.flags == Thread.IS_THREADS:
depth = 0
else:
depth = -1
result = self.format_message(obj.backend, obj.root, depth)
return result
def format_message(self, backend, message, depth=0):
if not message:
return u''
self.count += 1
flags = '['
if message.flags & message.IS_UNREAD:
flags += 'N'
else:
flags += '-'
if message.flags & message.IS_NOT_RECEIVED:
flags += 'U'
elif message.flags & message.IS_RECEIVED:
flags += 'R'
else:
flags += '-'
flags += ']'
if self.interactive:
result = u'%s%s* (%d)%s %s <%s> %s (%s)\n' % (depth * ' ',
self.BOLD,
self.count,
self.NC,
flags,
message.sender,
message.title,
backend)
else:
result = u'%s%s* (%s.%s@%s)%s %s <%s> %s\n' % (depth * ' ',
self.BOLD,
message.thread.id,
message.id,
backend,
self.NC,
flags,
message.sender,
message.title)
if message.children:
if depth >= 0:
depth += 1
for m in message.children:
result += self.format_message(backend, m, depth)
return result
class ProfileFormatter(IFormatter):
def flush(self):
pass
def format_obj(self, obj, alias=None):
return obj.get_text()
class Boobmsg(ReplApplication):
APPNAME = 'boobmsg'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Christophe Benz'
DESCRIPTION = "Console application allowing to send messages on various websites and " \
"to display message threads and contents."
SHORT_DESCRIPTION = "send and receive message threads"
CAPS = CapMessages
EXTRA_FORMATTERS = {'msglist': MessagesListFormatter,
'msg': MessageFormatter,
'xhtml': XHtmlFormatter,
'atom': AtomFormatter,
'profile' : ProfileFormatter,
}
COMMANDS_FORMATTERS = {'list': 'msglist',
'show': 'msg',
'export_thread': 'msg',
'export_all': 'msg',
'ls': 'msglist',
'profile': 'profile',
}
def add_application_options(self, group):
group.add_option('-E', '--accept-empty', action='store_true',
help='Send messages with an empty body.')
group.add_option('-t', '--title', action='store',
help='For the "post" command, set a title to message',
type='string', dest='title')
def load_default_backends(self):
self.load_backends(CapMessages, storage=self.create_storage())
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def do_status(self, line):
"""
status
Display status information about a backend.
"""
if len(line) > 0:
backend_name = line
else:
backend_name = None
results = {}
for field in self.do('get_account_status',
backends=backend_name,
caps=CapAccount):
if field.backend in results:
results[field.backend].append(field)
else:
results[field.backend] = [field]
for name, fields in results.items():
print(':: %s ::' % name)
for f in fields:
if f.flags & f.FIELD_HTML:
value = html2text(f.value)
else:
value = f.value
print('%s: %s' % (f.label, value))
print('')
def do_post(self, line):
"""
post RECEIVER@BACKEND[,RECEIVER@BACKEND[...]] [TEXT]
Post a message to the specified receivers.
Multiple receivers are separated by a comma.
If no text is supplied on command line, the content of message is read on stdin.
"""
receivers, text = self.parse_command_args(line, 2, 1)
if text is None:
text = self.acquire_input()
if not self.options.accept_empty and not text.strip():
self.logger.warning(u'The message body is empty, use option --accept_empty to send empty messages')
return
for receiver in receivers.strip().split(','):
receiver, backend_name = self.parse_id(receiver.strip(),
unique_backend=True)
if not backend_name and len(self.enabled_backends) > 1:
self.logger.warning(u'No backend specified for receiver "%s": message will be sent with all the '
'enabled backends (%s)' % (receiver,
','.join(backend.name for backend in self.enabled_backends)))
if '.' in receiver:
# It's a reply
thread_id, parent_id = receiver.rsplit('.', 1)
else:
# It's an original message
thread_id = receiver
parent_id = None
try:
thread_id = self.threads[int(thread_id) - 1].id
except (IndexError,ValueError):
pass
thread = Thread(thread_id)
message = Message(thread,
0,
title=self.options.title,
parent=Message(thread, parent_id) if parent_id else None,
content=text)
try:
self.do('post_message', message, backends=backend_name).wait()
except CallErrors as errors:
self.bcall_errors_handler(errors)
else:
if self.interactive:
print('Message sent sucessfully to %s' % receiver)
threads = []
messages = []
@defaultcount(10)
def do_list(self, arg):
"""
list
Display all threads.
"""
if len(arg) > 0:
try:
thread = self.threads[int(arg) - 1]
except (IndexError, ValueError):
id, backend_name = self.parse_id(arg)
else:
id = thread.id
backend_name = thread.backend
self.messages = []
cmd = self.do('get_thread', id, backends=backend_name)
self.formatter._list_messages = True
else:
self.threads = []
cmd = self.do('iter_threads')
self.formatter._list_messages = False
self.start_format()
for thread in cmd:
if not thread:
continue
if len(arg) > 0:
for m in thread.iter_all_messages():
if not m.backend:
m.backend = thread.backend
self.messages.append(m)
else:
self.threads.append(thread)
self.format(thread)
def do_export_all(self, arg):
"""
export_all
Export All threads
"""
def func(backend):
for thread in backend.iter_threads():
if not thread:
continue
t = backend.fillobj(thread, None)
for msg in t.iter_all_messages():
yield msg
self.start_format()
for msg in self.do(func):
self.format(msg)
def do_export_thread(self, arg):
"""
export_thread ID
Export the thread identified by ID
"""
try:
thread = self.threads[int(arg) - 1]
except (IndexError, ValueError):
_id, backend_name = self.parse_id(arg)
else:
_id = thread.id
backend_name = thread.backend
cmd = self.do('get_thread', _id, backends=backend_name)
self.start_format()
for thread in cmd:
if thread is not None:
thread, = self.do('fillobj', thread, None, backends=thread.backend)
for msg in thread.iter_all_messages():
msg, = self.do('fillobj', msg, None, backends=thread.backend)
self.format(msg)
def do_show(self, arg):
"""
show MESSAGE
Read a message
"""
message = None
if len(arg) == 0:
print('Please give a message ID.', file=self.stderr)
return 2
try:
message = self.messages[int(arg) - 1]
except (IndexError, ValueError):
# The message is not is the cache, we have now two cases:
# 1) the user uses a number to get a thread in the cache
# 2) the user gives a thread id
try:
thread = self.threads[int(arg) - 1]
if not empty(thread.root):
message = thread.root
else:
for thread in self.do('get_thread', thread.id, backends=thread.backend):
if thread is not None:
if not thread.root:
thread, = self.do('fillobj', thread, ('root',), backends=thread.backend)
message = thread.root
except (IndexError, ValueError):
_id, backend_name = self.parse_id(arg)
for thread in self.do('get_thread', _id, backends=backend_name):
if thread is not None:
if not thread.root:
thread, = self.do('fillobj', thread, ('root',), backends=thread.backend)
message = thread.root
if not empty(message):
self.start_format()
self.format(message)
self.weboob.do('set_message_read', message, backends=message.backend)
return
else:
print('Message not found', file=self.stderr)
return 3
def do_profile(self, id):
"""
profile ID
Display a profile
"""
_id, backend_name = self.parse_id(id, unique_backend=True)
found = 0
for contact in self.do('get_contact', _id, backends=backend_name, caps=CapContact):
if contact:
self.format(contact)
found = 1
if not found:
self.logger.error(u'Profile not found')
def do_photos(self, id):
"""
photos ID
Display photos of a profile
"""
photo_cmd = self.config.get('photo_viewer')
if photo_cmd is None:
print("Configuration error: photo_viewer is undefined", file=self.stderr)
return
_id, backend_name = self.parse_id(id, unique_backend=True)
found = 0
for contact in self.do('get_contact', _id, backends=backend_name):
if contact:
# Write photo to temporary files
tmp_files = []
for photo in contact.photos.values():
suffix = '.jpg'
if '.' in photo.url.split('/')[-1]:
suffix = '.%s' % photo.url.split('/')[-1].split('.')[-1]
f = NamedTemporaryFile(suffix=suffix)
photo = self.weboob[contact.backend].fillobj(photo, 'data')
f.write(photo.data)
tmp_files.append(f)
os.system(photo_cmd % ' '.join([file.name for file in tmp_files]))
found = 1
if not found:
self.logger.error(u'Profile not found')
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobooks/ 0000775 0000000 0000000 00000000000 13204006733 0026100 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobooks/__init__.py 0000664 0000000 0000000 00000001431 13204006733 0030210 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Jérémy Monnet
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobooks import Boobooks
__all__ = ['Boobooks']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobooks/boobooks.py 0000664 0000000 0000000 00000005160 13204006733 0030271 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2009-2012 Jeremy Monnet
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.library import CapBook, Book
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Boobooks']
class RentedListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'date', 'author', 'name', 'late')
RED = '[1;31m'
def get_title(self, obj):
s = u'%s — %s (%s)' % (obj.author, obj.name, obj.date)
if obj.late:
s += u' %sLATE!%s' % (self.RED, self.NC)
return s
class Boobooks(ReplApplication):
APPNAME = 'boobooks'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Jeremy Monnet'
CAPS = CapBook
DESCRIPTION = "Console application allowing to list your books rented or booked at the library, " \
"book and search new ones, get your booking history (if available)."
SHORT_DESCRIPTION = "manage rented books"
EXTRA_FORMATTERS = {'rented_list': RentedListFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'ls': 'rented_list',
'list': 'rented_list',
'rented': 'rented_list',
}
COLLECTION_OBJECTS = (Book, )
def do_renew(self, id):
"""
renew ID
Renew a book
"""
id, backend_name = self.parse_id(id)
if not id:
print('Error: please give a book ID (hint: use ls command)', file=self.stderr)
return 2
names = (backend_name,) if backend_name is not None else None
for renew in self.do('renew_book', id, backends=names):
self.format(renew)
def do_rented(self, args):
"""
rented
List rented books
"""
for book in self.do('iter_rented', backends=None):
self.format(book)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobsize/ 0000775 0000000 0000000 00000000000 13204006733 0026077 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobsize/__init__.py 0000664 0000000 0000000 00000001425 13204006733 0030212 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobsize import Boobsize
__all__ = ['Boobsize']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobsize/boobsize.py 0000664 0000000 0000000 00000015736 13204006733 0030301 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.base import empty
from weboob.capabilities.gauge import CapGauge, SensorNotFound
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
from weboob.tools.compat import unicode
__all__ = ['Boobsize']
class GaugeFormatter(IFormatter):
MANDATORY_FIELDS = ('name', 'object', 'sensors')
DISPLAYED_FIELDS = ('city', )
def start_format(self, **kwargs):
# Name = 27 Object = 10 City = 10 Sensors = 33
self.output(' Name and ID Object City Sensors ')
self.output('----------------------------+----------+----------+---------------------------------')
def format_obj(self, obj, alias):
name = obj.name
city = u""
if not empty(obj.city):
city = obj.city
if not obj.sensors or (len(obj.sensors) == 0):
result = u' %s %s %s \n' %\
(self.colored('%-27s' % name[:27], 'red'),
self.colored('%-10s' % obj.object[:10], 'yellow'),
self.colored('%-10s' % city[:10], 'yellow')
)
result += u' %s \n' % self.colored('%-47s' % obj.fullid[:47], 'blue')
else:
first = True
firstaddress = obj.sensors[0].address
for sensor in obj.sensors:
sensorname = sensor.name
# This is a int value, do not display it as a float
if not empty(sensor.lastvalue.level):
if int(sensor.lastvalue.level) == sensor.lastvalue.level:
lastvalue = "%d " % sensor.lastvalue.level
else:
lastvalue = "%r " % sensor.lastvalue.level
if not empty(sensor.unit):
lastvalue += "%s" % sensor.unit
else:
lastvalue = u"? "
if first:
result = u' %s %s %s ' %\
(self.colored('%-27s' % name[:27], 'red'),
self.colored('%-10s' % obj.object[:10], 'yellow'),
self.colored('%-10s' % city[:10], 'yellow'),
)
if not empty(firstaddress):
result += u'%s' % self.colored('%-33s' % sensor.address[:33], 'yellow')
result += u'\n'
result += u' %s' % self.colored('%-47s' % obj.fullid[:47], 'blue')
result += u' %s %s\n' %\
(self.colored('%-20s' % sensorname[:20], 'magenta'),
self.colored('%-13s' % lastvalue[:13], 'red')
)
first = False
else:
result += u' %s %s\n' %\
(self.colored('%-20s' % sensorname[:20], 'magenta'),
self.colored('%-13s' % lastvalue[:13], 'red')
)
if not empty(sensor.address) and sensor.address != firstaddress:
result += u' %s \n' %\
self.colored('%-33s' % sensor.address[:33], 'yellow')
return result
class Boobsize(ReplApplication):
APPNAME = 'Boobsize'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Florent Fourcot'
DESCRIPTION = "Console application allowing to display various sensors and gauges values."
SHORT_DESCRIPTION = "display sensors and gauges values"
CAPS = (CapGauge)
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'gauge_list': GaugeFormatter, }
COMMANDS_FORMATTERS = {'search': 'gauge_list',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def bcall_error_handler(self, backend, error, backtrace):
if isinstance(error, SensorNotFound):
msg = unicode(error) or 'Sensor not found (hint: try details command)'
print('Error(%s): %s' % (backend.name, msg), file=self.stderr)
else:
return ReplApplication.bcall_error_handler(self, backend, error, backtrace)
def do_search(self, pattern):
"""
search [PATTERN]
Display all gauges. If PATTERN is specified, search on a pattern.
"""
self.change_path([u'gauges'])
self.start_format()
for gauge in self.do('iter_gauges', pattern or None, caps=CapGauge):
self.cached_format(gauge)
def complete_search(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_details(self, line):
"""
details GAUGE_ID
Display details of all sensors of the gauge.
"""
gauge, pattern = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(gauge)
self.start_format()
for sensor in self.do('iter_sensors', _id, pattern=pattern, backends=backend_name, caps=CapGauge):
self.format(sensor)
def do_history(self, line):
"""
history SENSOR_ID
Get history of a specific sensor (use 'search' to find a gauge, and sensors GAUGE_ID to list sensors attached to the gauge).
"""
gauge, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(gauge)
self.start_format()
for measure in self.do('iter_gauge_history', _id, backends=backend_name, caps=CapGauge):
self.format(measure)
def complete_last_sensor_measure(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_last_sensor_measure(self, line):
"""
last_sensor_measure SENSOR_ID
Get last measure of a sensor.
"""
gauge, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(gauge)
self.start_format()
for measure in self.do('get_last_measure', _id, backends=backend_name, caps=CapGauge):
self.format(measure)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobtracker/ 0000775 0000000 0000000 00000000000 13204006733 0026560 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobtracker/__init__.py 0000664 0000000 0000000 00000001433 13204006733 0030672 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobtracker import BoobTracker
__all__ = ['BoobTracker']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/boobtracker/boobtracker.py 0000664 0000000 0000000 00000043020 13204006733 0031426 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import timedelta
from email import message_from_string, message_from_file
from email.Header import decode_header
from email.mime.text import MIMEText
from smtplib import SMTP
import os
import re
import unicodedata
from weboob.capabilities.base import empty, BaseObject
from weboob.capabilities.bugtracker import CapBugTracker, Query, Update, Project, Issue, IssueError
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.tools.compat import basestring, unicode
from weboob.tools.html import html2text
from weboob.tools.date import parse_french_date
__all__ = ['BoobTracker']
try:
input = raw_input
except NameError:
pass
class IssueFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'project', 'title', 'body', 'author')
def format_attr(self, obj, attr):
if not hasattr(obj, attr) or empty(getattr(obj, attr)):
return u''
value = getattr(obj, attr)
if isinstance(value, BaseObject):
value = value.name
return self.format_key(attr.capitalize(), value)
def format_key(self, key, value):
return '%s %s\n' % (self.colored('%s:' % key, 'green'),
value)
def format_obj(self, obj, alias):
result = u'%s %s %s %s %s\n' % (self.colored(obj.project.name, 'blue', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(obj.fullid, 'red', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(obj.title, 'yellow', 'bold'))
result += '\n%s\n\n' % obj.body
result += self.format_key('Author', '%s (%s)' % (obj.author.name, obj.creation))
result += self.format_attr(obj, 'status')
result += self.format_attr(obj, 'priority')
result += self.format_attr(obj, 'version')
result += self.format_attr(obj, 'tracker')
result += self.format_attr(obj, 'category')
result += self.format_attr(obj, 'assignee')
if hasattr(obj, 'fields') and not empty(obj.fields):
for key, value in obj.fields.items():
result += self.format_key(key.capitalize(), value)
if hasattr(obj, 'attachments') and obj.attachments:
result += '\n%s\n' % self.colored('Attachments:', 'green')
for a in obj.attachments:
result += '* %s%s%s <%s>\n' % (self.BOLD, a.filename, self.NC, a.url)
if hasattr(obj, 'history') and obj.history:
result += '\n%s\n' % self.colored('History:', 'green')
for u in obj.history:
result += '%s %s %s %s\n' % (self.colored('*', 'red', 'bold'),
self.colored(u.date, 'yellow', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(u.author.name, 'blue', 'bold'))
for change in u.changes:
result += ' - %s %s %s %s\n' % (self.colored(change.field, 'green'),
change.last,
self.colored('->', 'magenta'), change.new)
if u.message:
result += ' %s\n' % html2text(u.message).strip().replace('\n', '\n ')
return result
class IssuesListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'project', 'status', 'title', 'category')
def get_title(self, obj):
return '%s - [%s] %s' % (obj.project.name, obj.status.name, obj.title)
def get_description(self, obj):
return obj.category
class BoobTracker(ReplApplication):
APPNAME = 'boobtracker'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2011-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to create, edit, view bug tracking issues."
SHORT_DESCRIPTION = "manage bug tracking issues"
CAPS = CapBugTracker
EXTRA_FORMATTERS = {'issue_info': IssueFormatter,
'issues_list': IssuesListFormatter,
}
COMMANDS_FORMATTERS = {'get': 'issue_info',
'post': 'issue_info',
'edit': 'issue_info',
'search': 'issues_list',
'ls': 'issues_list',
}
COLLECTION_OBJECTS = (Project, Issue, )
def add_application_options(self, group):
group.add_option('--author')
group.add_option('--title')
group.add_option('--assignee')
group.add_option('--target-version', dest='version')
group.add_option('--tracker')
group.add_option('--category')
group.add_option('--status')
group.add_option('--priority')
group.add_option('--start')
group.add_option('--due')
@defaultcount(10)
def do_search(self, line):
"""
search PROJECT
List issues for a project.
You can use these filters from command line:
--author AUTHOR
--title TITLE_PATTERN
--assignee ASSIGNEE
--target-version VERSION
--category CATEGORY
--status STATUS
"""
query = Query()
path = self.working_path.get()
backends = []
if line.strip():
query.project, backends = self.parse_id(line, unique_backend=True)
elif len(path) > 0:
query.project = path[0]
else:
print('Please enter a project name', file=self.stderr)
return 1
query.author = self.options.author
query.title = self.options.title
query.assignee = self.options.assignee
query.version = self.options.version
query.category = self.options.category
query.status = self.options.status
self.change_path([query.project, u'search'])
for issue in self.do('iter_issues', query, backends=backends):
self.add_object(issue)
self.format(issue)
def complete_get(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_get(self, line):
"""
get ISSUE
Get an issue and display it.
"""
if not line:
print('This command takes an argument: %s' % self.get_command_help('get', short=True), file=self.stderr)
return 2
issue = self.get_object(line, 'get_issue')
if not issue:
print('Issue not found: %s' % line, file=self.stderr)
return 3
self.format(issue)
def complete_comment(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_comment(self, line):
"""
comment ISSUE [TEXT]
Comment an issue. If no text is given, enter it in standard input.
"""
id, text = self.parse_command_args(line, 2, 1)
if text is None:
text = self.acquire_input()
id, backend_name = self.parse_id(id, unique_backend=True)
update = Update(0)
update.message = text
self.do('update_issue', id, update, backends=backend_name).wait()
def do_logtime(self, line):
"""
logtime ISSUE HOURS [TEXT]
Log spent time on an issue.
"""
id, hours, text = self.parse_command_args(line, 3, 2)
if text is None:
text = self.acquire_input()
try:
hours = float(hours)
except ValueError:
print('Error: HOURS parameter may be a float', file=self.stderr)
return 1
id, backend_name = self.parse_id(id, unique_backend=True)
update = Update(0)
update.message = text
update.hours = timedelta(hours=hours)
self.do('update_issue', id, update, backends=backend_name).wait()
def complete_remove(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_remove(self, line):
"""
remove ISSUE
Remove an issue.
"""
id, backend_name = self.parse_id(line, unique_backend=True)
self.do('remove_issue', id, backends=backend_name).wait()
ISSUE_FIELDS = (('title', (None, False)),
('assignee', ('members', True)),
('version', ('versions', True)),
('tracker', (None, False)),#XXX
('category', ('categories', False)),
('status', ('statuses', True)),
('priority', (None, False)),#XXX
('start', (None, False)),
('due', (None, False)),
)
def get_list_item(self, objects_list, name):
if name is None:
return None
for obj in objects_list:
if obj.name.lower() == name.lower():
return obj
if not name:
return None
raise ValueError('"%s" is not found' % name)
def sanitize_key(self, key):
if isinstance(key, str):
key = unicode(key, "utf8")
key = unicodedata.normalize('NFKD', key).encode("ascii", "ignore")
return key.replace(' ', '-').capitalize()
def issue2text(self, issue, backend=None):
if backend is not None and 'username' in backend.config:
sender = backend.config['username'].get()
else:
sender = os.environ.get('USERNAME', 'boobtracker')
output = u'From: %s\n' % sender
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
value = None
if not self.interactive:
value = getattr(self.options, key)
if not value:
value = getattr(issue, key)
if not value:
value = ''
elif hasattr(value, 'name'):
value = value.name
if list_name is not None:
objects_list = getattr(issue.project, list_name)
if len(objects_list) == 0:
continue
output += '%s: %s\n' % (self.sanitize_key(key), value)
if list_name is not None:
availables = ', '.join(['<%s>' % (o if isinstance(o, basestring) else o.name)
for o in objects_list])
output += 'X-Available-%s: %s\n' % (self.sanitize_key(key), availables)
for key, value in issue.fields.items():
output += '%s: %s\n' % (self.sanitize_key(key), value or '')
# TODO: Add X-Available-* for lists
output += '\n%s' % (issue.body or 'Please write your bug report here.')
return output
def text2issue(self, issue, m):
# XXX HACK to support real incoming emails
if 'Subject' in m:
m['Title'] = m['Subject']
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
value = m.get(key)
if value is None:
continue
new_value = u''
for part in decode_header(value):
if part[1]:
new_value += unicode(part[0], part[1])
else:
new_value += part[0].decode('utf-8')
value = new_value
if is_list_object:
objects_list = getattr(issue.project, list_name)
value = self.get_list_item(objects_list, value)
# FIXME: autodetect
if key in ['start', 'due']:
if len(value) > 0:
#value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
value = parse_french_date(value)
else:
value = None
setattr(issue, key, value)
for key in issue.fields.keys():
value = m.get(self.sanitize_key(key))
if value is not None:
issue.fields[key] = value.decode('utf-8')
content = u''
for part in m.walk():
if part.get_content_type() == 'text/plain':
s = part.get_payload(decode=True)
charsets = part.get_charsets() + m.get_charsets()
for charset in charsets:
try:
if charset is not None:
content += unicode(s, charset)
else:
content += unicode(s, encoding='utf-8')
except UnicodeError as e:
self.logger.warning('Unicode error: %s' % e)
continue
except Exception as e:
self.logger.exception(e)
continue
else:
break
issue.body = content
m = re.search('([^< ]+@[^ >]+)', m['From'] or '')
if m:
return m.group(1)
def edit_issue(self, issue, edit=True):
backend = self.weboob.get_backend(issue.backend)
content = self.issue2text(issue, backend)
while True:
if self.stdin.isatty():
content = self.acquire_input(content, {'vim': "-c 'set ft=mail'"})
m = message_from_string(content.encode('utf-8'))
else:
m = message_from_file(self.stdin)
try:
email_to = self.text2issue(issue, m)
except ValueError as e:
if not self.stdin.isatty():
raise
input("%s -- Press Enter to continue..." % unicode(e).encode("utf-8"))
continue
try:
issue = backend.post_issue(issue)
print('Issue %s %s' % (self.formatter.colored(issue.fullid, 'red', 'bold'),
'updated' if edit else 'created'))
if edit:
self.format(issue)
elif email_to:
self.send_notification(email_to, issue)
return 0
except IssueError as e:
if not self.stdin.isatty():
raise
input("%s -- Press Enter to continue..." % unicode(e).encode("utf-8"))
def send_notification(self, email_to, issue):
text = """Hi,
You have successfuly created this ticket on the Weboob tracker:
%s
You can follow your bug report on this page:
https://symlink.me/issues/%s
Regards,
Weboob Team
""" % (issue.title, issue.id)
msg = MIMEText(text, 'plain', 'utf-8')
msg['Subject'] = 'Issue #%s reported' % issue.id
msg['From'] = 'Weboob '
msg['To'] = email_to
s = SMTP('localhost')
s.sendmail('weboob@weboob.org', [email_to], msg.as_string())
s.quit()
def do_post(self, line):
"""
post PROJECT
Post a new issue.
If you are not in interactive mode, you can use these parameters:
--title TITLE
--assignee ASSIGNEE
--target-version VERSION
--category CATEGORY
--status STATUS
"""
if not line.strip():
print('Please give the project name')
return 1
project, backend_name = self.parse_id(line, unique_backend=True)
backend = self.weboob.get_backend(backend_name)
issue = backend.create_issue(project)
issue.backend = backend.name
return self.edit_issue(issue, edit=False)
def complete_edit(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
if len(args) == 3:
return list(dict(self.ISSUE_FIELDS).keys())
def do_edit(self, line):
"""
edit ISSUE [KEY [VALUE]]
Edit an issue.
If you are not in interactive mode, you can use these parameters:
--title TITLE
--assignee ASSIGNEE
--target-version VERSION
--category CATEGORY
--status STATUS
"""
_id, key, value = self.parse_command_args(line, 3, 1)
issue = self.get_object(_id, 'get_issue')
if not issue:
print('Issue not found: %s' % _id, file=self.stderr)
return 3
return self.edit_issue(issue, edit=True)
def complete_attach(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_attach(self, line):
"""
attach ISSUE FILENAME
Attach a file to an issue (Not implemented yet).
"""
print('Not implemented yet.', file=self.stderr)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/cineoob/ 0000775 0000000 0000000 00000000000 13204006733 0025701 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/cineoob/__init__.py 0000664 0000000 0000000 00000001421 13204006733 0030010 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .cineoob import Cineoob
__all__ = ['Cineoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/cineoob/cineoob.py 0000664 0000000 0000000 00000064011 13204006733 0027673 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import datetime
from weboob.applications.weboorrents.weboorrents import TorrentInfoFormatter, TorrentListFormatter
from weboob.applications.suboob.suboob import SubtitleInfoFormatter, SubtitleListFormatter
from weboob.capabilities.torrent import CapTorrent, MagnetOnly
from weboob.capabilities.cinema import CapCinema
from weboob.capabilities.subtitle import CapSubtitle
from weboob.capabilities.base import empty, NotAvailable
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.core import CallErrors
__all__ = ['Cineoob']
ROLE_LIST = ['actor', 'director', 'writer', 'composer', 'producer']
COUNTRY_LIST = ['us', 'fr', 'de', 'jp']
class MovieInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'original_title', 'release_date',
'other_titles', 'duration', 'pitch', 'note', 'roles', 'country')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.original_title, self.NC)
result += 'ID: %s\n' % obj.fullid
if not empty(obj.release_date):
result += 'Released: %s\n' % obj.release_date.strftime('%Y-%m-%d')
result += 'Country: %s\n' % obj.country
if not empty(obj.duration):
result += 'Duration: %smin\n' % obj.duration
result += 'Note: %s\n' % obj.note
if not empty(obj.genres):
result += '\n%sGenres%s\n' % (self.BOLD, self.NC)
for g in obj.genres:
result += ' * %s\n' % g
if not empty(obj.roles):
result += '\n%sRelated persons%s\n' % (self.BOLD, self.NC)
for role, lpersons in obj.roles.items():
result += ' -- %s\n' % role
for person in lpersons:
result += ' * %s\n' % person[1]
if not empty(obj.other_titles):
result += '\n%sOther titles%s\n' % (self.BOLD, self.NC)
for t in obj.other_titles:
result += ' * %s\n' % t
if not empty(obj.pitch):
result += '\n%sStory%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.pitch
return result
class MovieListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'original_title', 'short_description')
def get_title(self, obj):
return obj.original_title
def get_description(self, obj):
result = u''
if not empty(obj.short_description):
result = obj.short_description
return result
class MovieReleasesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'original_title', 'all_release_dates')
def get_title(self, obj):
return u'Releases of %s' % obj.original_title
def get_description(self, obj):
return u'\n%s' % obj.all_release_dates
def yearsago(years, from_date=None):
if from_date is None:
from_date = datetime.now()
try:
return from_date.replace(year=from_date.year - years)
except:
# Must be 2/29
assert from_date.month == 2 and from_date.day == 29
return from_date.replace(month=2, day=28,
year=from_date.year-years)
def num_years(begin, end=None):
if end is None:
end = datetime.now()
num_years = int((end - begin).days / 365.25)
if begin > yearsago(num_years, end):
return num_years - 1
else:
return num_years
class PersonInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name', 'birth_date', 'birth_place', 'short_biography')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.name, self.NC)
result += 'ID: %s\n' % obj.fullid
if not empty(obj.real_name):
result += 'Real name: %s\n' % obj.real_name
if not empty(obj.birth_place):
result += 'Birth place: %s\n' % obj.birth_place
if not empty(obj.birth_date):
result += 'Birth date: %s\n' % obj.birth_date.strftime('%Y-%m-%d')
if not empty(obj.death_date):
age = num_years(obj.birth_date, obj.death_date)
result += 'Death date: %s at %s years old\n' % (obj.death_date.strftime('%Y-%m-%d'), age)
else:
age = num_years(obj.birth_date)
result += 'Age: %s\n' % age
if not empty(obj.gender):
result += 'Gender: %s\n' % obj.gender
if not empty(obj.nationality):
result += 'Nationality: %s\n' % obj.nationality
if not empty(obj.roles):
result += '\n%sRelated movies%s\n' % (self.BOLD, self.NC)
for role, lmovies in obj.roles.items():
result += ' -- %s\n' % role
for movie in lmovies:
result += ' * %s\n' % movie[1]
if not empty(obj.short_biography):
result += '\n%sShort biography%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.short_biography
return result
class PersonListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'short_description')
def get_title(self, obj):
return obj.name
def get_description(self, obj):
result = u''
if not empty(obj.short_description):
result = obj.short_description
return result
class PersonBiographyFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'biography')
def get_title(self, obj):
return u'Biography of %s' % obj.name
def get_description(self, obj):
result = u'\n%s' % obj.biography
return result
class Cineoob(ReplApplication):
APPNAME = 'cineoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Julien Veyssier'
DESCRIPTION = "Console application allowing to search for movies and persons on various cinema databases " \
", list persons related to a movie, list movies related to a person and list common movies " \
"of two persons."
SHORT_DESCRIPTION = "search movies and persons around cinema"
CAPS = (CapCinema, CapTorrent, CapSubtitle)
EXTRA_FORMATTERS = {'movie_list': MovieListFormatter,
'movie_info': MovieInfoFormatter,
'movie_releases': MovieReleasesFormatter,
'person_list': PersonListFormatter,
'person_info': PersonInfoFormatter,
'person_bio': PersonBiographyFormatter,
'torrent_list': TorrentListFormatter,
'torrent_info': TorrentInfoFormatter,
'subtitle_list': SubtitleListFormatter,
'subtitle_info': SubtitleInfoFormatter
}
COMMANDS_FORMATTERS = {'search_movie': 'movie_list',
'info_movie': 'movie_info',
'search_person': 'person_list',
'info_person': 'person_info',
'casting': 'person_list',
'filmography': 'movie_list',
'biography': 'person_bio',
'releases': 'movie_releases',
'movies_in_common': 'movie_list',
'persons_in_common': 'person_list',
'search_torrent': 'torrent_list',
'search_movie_torrent': 'torrent_list',
'info_torrent': 'torrent_info',
'search_subtitle': 'subtitle_list',
'search_movie_subtitle': 'subtitle_list',
'info_subtitle': 'subtitle_info'
}
def complete_filmography(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 3:
return ROLE_LIST
def complete_casting(self, text, line, *ignored):
return self.complete_filmography(text, line, ignored)
def do_movies_in_common(self, line):
"""
movies_in_common person_ID person_ID
Get the list of common movies between two persons.
"""
id1, id2 = self.parse_command_args(line, 2, 1)
person1 = self.get_object(id1, 'get_person', caps=CapCinema)
if not person1:
print('Person not found: %s' % id1, file=self.stderr)
return 3
person2 = self.get_object(id2, 'get_person', caps=CapCinema)
if not person2:
print('Person not found: %s' % id2, file=self.stderr)
return 3
initial_count = self.options.count
self.options.count = None
lid1 = []
for id in self.do('iter_person_movies_ids', person1.id, caps=CapCinema):
lid1.append(id)
lid2 = []
for id in self.do('iter_person_movies_ids', person2.id, caps=CapCinema):
lid2.append(id)
self.options.count = initial_count
inter = list(set(lid1) & set(lid2))
chrono_list = []
for common in inter:
movie = self.get_object(common, 'get_movie', caps=CapCinema)
role1 = movie.get_roles_by_person_id(person1.id)
if not role1:
role1 = movie.get_roles_by_person_name(person1.name)
role2 = movie.get_roles_by_person_id(person2.id)
if not role2:
role2 = movie.get_roles_by_person_name(person2.name)
if (movie.release_date != NotAvailable):
year = movie.release_date.year
else:
year = '????'
movie.short_description = '(%s) %s as %s ; %s as %s'%(year, person1.name, ', '.join(role1), person2.name, ', '.join(role2))
if movie:
i = 0
while (i chrono_list[i].release_date.year)):
i += 1
chrono_list.insert(i, movie)
for movie in chrono_list:
self.cached_format(movie)
def do_persons_in_common(self, line):
"""
persons_in_common movie_ID movie_ID
Get the list of common persons between two movies.
"""
id1, id2 = self.parse_command_args(line, 2, 1)
movie1 = self.get_object(id1, 'get_movie', caps=CapCinema)
if not movie1:
print('Movie not found: %s' % id1, file=self.stderr)
return 3
movie2 = self.get_object(id2, 'get_movie', caps=CapCinema)
if not movie2:
print('Movie not found: %s' % id2, file=self.stderr)
return 3
initial_count = self.options.count
self.options.count = None
lid1 = []
for id in self.do('iter_movie_persons_ids', movie1.id, caps=CapCinema):
lid1.append(id)
lid2 = []
for id in self.do('iter_movie_persons_ids', movie2.id, caps=CapCinema):
lid2.append(id)
self.options.count = initial_count
inter = list(set(lid1) & set(lid2))
for common in inter:
person = self.get_object(common, 'get_person', caps=CapCinema)
role1 = person.get_roles_by_movie_id(movie1.id)
if not role1:
role1 = person.get_roles_by_movie_title(movie1.original_title)
role2 = person.get_roles_by_movie_id(movie2.id)
if not role2:
role2 = person.get_roles_by_movie_title(movie2.original_title)
person.short_description = '%s in %s ; %s in %s'%(', '.join(role1), movie1.original_title, ', '.join(role2), movie2.original_title)
self.cached_format(person)
def do_info_movie(self, id):
"""
info_movie movie_ID
Get information about a movie.
"""
movie = self.get_object(id, 'get_movie', caps=CapCinema)
if not movie:
print('Movie not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(movie)
def do_info_person(self, id):
"""
info_person person_ID
Get information about a person.
"""
person = self.get_object(id, 'get_person', caps=CapCinema)
if not person:
print('Person not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(person)
@defaultcount(10)
def do_search_movie(self, pattern):
"""
search_movie [PATTERN]
Search movies.
"""
self.change_path([u'search movies'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for movie in self.do('iter_movies', pattern=pattern, caps=CapCinema):
self.cached_format(movie)
@defaultcount(10)
def do_search_person(self, pattern):
"""
search_person [PATTERN]
Search persons.
"""
self.change_path([u'search persons'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for person in self.do('iter_persons', pattern=pattern, caps=CapCinema):
self.cached_format(person)
def do_casting(self, line):
"""
casting movie_ID [ROLE]
List persons related to a movie.
If ROLE is given, filter by ROLE
"""
movie_id, role = self.parse_command_args(line, 2, 1)
movie = self.get_object(movie_id, 'get_movie', caps=CapCinema)
if not movie:
print('Movie not found: %s' % id, file=self.stderr)
return 3
for person in self.do('iter_movie_persons', movie.id, role, backends=movie.backend, caps=CapCinema):
self.cached_format(person)
def do_filmography(self, line):
"""
filmography person_ID [ROLE]
List movies of a person.
If ROLE is given, filter by ROLE
"""
person_id, role = self.parse_command_args(line, 2, 1)
person = self.get_object(person_id, 'get_person', caps=CapCinema)
if not person:
print('Person not found: %s' % id, file=self.stderr)
return 3
for movie in self.do('iter_person_movies', person.id, role, backends=person.backend, caps=CapCinema):
self.cached_format(movie)
def do_biography(self, person_id):
"""
biography person_ID
Show the complete biography of a person.
"""
person = self.get_object(person_id, 'get_person', ('name', 'biography'), caps=CapCinema)
if not person:
print('Person not found: %s' % person_id, file=self.stderr)
return 3
self.start_format()
self.format(person)
def complete_releases(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
if len(args) == 3:
return COUNTRY_LIST
def do_releases(self, line):
"""
releases movie_ID [COUNTRY]
Get releases dates of a movie.
If COUNTRY is given, show release in this country.
"""
id, country = self.parse_command_args(line, 2, 1)
movie = self.get_object(id, 'get_movie', ('original_title'), caps=CapCinema)
if not movie:
print('Movie not found: %s' % id, file=self.stderr)
return 3
# i would like to clarify with fillobj but how could i fill the movie AND choose the country ?
for release in self.do('get_movie_releases', movie.id, country, caps=CapCinema, backends=movie.backend):
if not empty(release):
movie.all_release_dates = u'%s' % (release)
else:
print('Movie releases not found for %s' % movie.original_title, file=self.stderr)
return 3
self.start_format()
self.format(movie)
# ================== TORRENT ==================
def complete_info_torrent(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info_torrent(self, id):
"""
info_torrent ID
Get information about a torrent.
"""
torrent = self.get_object(id, 'get_torrent', caps=CapTorrent)
if not torrent:
print('Torrent not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(torrent)
def complete_getfile_torrent(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_getfile_torrent(self, line):
"""
getfile_torrent ID [FILENAME]
Get the .torrent file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(id)
if dest is None:
dest = '%s.torrent' % _id
try:
for buf in self.do('get_torrent_file', _id, backends=backend_name, caps=CapTorrent):
if buf:
if dest == '-':
print(buf)
else:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write .torrent in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MagnetOnly):
print(u'Error(%s): No direct URL available, '
u'please provide this magnet URL '
u'to your client:\n%s' % (backend, error.magnet), file=self.stderr)
return 4
else:
self.bcall_error_handler(backend, error, backtrace)
print('Torrent "%s" not found' % id, file=self.stderr)
return 3
@defaultcount(10)
def do_search_torrent(self, pattern):
"""
search_torrent [PATTERN]
Search torrents.
"""
self.change_path([u'search torrent'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for torrent in self.do('iter_torrents', pattern=pattern, caps=CapTorrent):
self.cached_format(torrent)
@defaultcount(10)
def do_search_movie_torrent(self, id):
"""
search_movie_torrent movie_ID
Search torrents of movie_ID.
"""
movie = self.get_object(id, 'get_movie', ('original_title'), caps=CapCinema)
if not movie:
print('Movie not found: %s' % id, file=self.stderr)
return 3
pattern = movie.original_title
self.change_path([u'search torrent'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for torrent in self.do('iter_torrents', pattern=pattern, caps=CapTorrent):
self.cached_format(torrent)
# ================== SUBTITLE ==================
def complete_info_subtitle(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info_subtitle(self, id):
"""
info_subtitle subtitle_ID
Get information about a subtitle.
"""
subtitle = self.get_object(id, 'get_subtitle', caps=CapCinema)
if not subtitle:
print('Subtitle not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(subtitle)
def complete_getfile_subtitle(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_getfile_subtitle(self, line):
"""
getfile_subtitle subtitle_ID [FILENAME]
Get the subtitle or archive file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(id)
if dest is None:
dest = '%s' % _id
for buf in self.do('get_subtitle_file', _id, backends=backend_name, caps=CapSubtitle):
if buf:
if dest == '-':
print(buf)
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print('Unable to write file in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
print('Subtitle "%s" not found' % id, file=self.stderr)
return 3
@defaultcount(10)
def do_search_subtitle(self, line):
"""
search_subtitle language [PATTERN]
Search subtitles.
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
language, pattern = self.parse_command_args(line, 2, 1)
self.change_path([u'search subtitle'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for subtitle in self.do('iter_subtitles', language=language, pattern=pattern, caps=CapSubtitle):
self.cached_format(subtitle)
@defaultcount(10)
def do_search_movie_subtitle(self, line):
"""
search_movie_subtitle language movie_ID
Search subtitles of movie_ID.
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
language, id = self.parse_command_args(line, 2, 2)
movie = self.get_object(id, 'get_movie', ('original_title'), caps=CapCinema)
if not movie:
print('Movie not found: %s' % id, file=self.stderr)
return 3
pattern = movie.original_title
self.change_path([u'search subtitle'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for subtitle in self.do('iter_subtitles', language=language, pattern=pattern, caps=CapSubtitle):
self.cached_format(subtitle)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/comparoob/ 0000775 0000000 0000000 00000000000 13204006733 0026244 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/comparoob/__init__.py 0000664 0000000 0000000 00000000123 13204006733 0030351 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from .comparoob import Comparoob
__all__ = ['Comparoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/comparoob/comparoob.py 0000664 0000000 0000000 00000014047 13204006733 0030605 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.pricecomparison import CapPriceComparison
from weboob.tools.html import html2text
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.tools.application.base import MoreResultsAvailable
from weboob.tools.application.console import ConsoleApplication
__all__ = ['Comparoob']
class PriceFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'cost', 'currency', 'shop', 'product')
def format_obj(self, obj, alias):
if hasattr(obj, 'message') and obj.message:
message = obj.message
else:
message = u'%s (%s)' % (obj.shop.name, obj.shop.location)
result = u'%s%s%s\n' % (self.BOLD, message, self.NC)
result += u'ID: %s\n' % obj.fullid
result += u'Product: %s\n' % obj.product.name
result += u'Cost: %s%s\n' % (obj.cost, obj.currency)
if hasattr(obj, 'date') and obj.date:
result += u'Date: %s\n' % obj.date.strftime('%Y-%m-%d')
result += u'\n%sShop:%s\n' % (self.BOLD, self.NC)
result += u'\tName: %s\n' % obj.shop.name
if obj.shop.location:
result += u'\tLocation: %s\n' % obj.shop.location
if obj.shop.info:
result += u'\n\t' + html2text(obj.shop.info).replace('\n', '\n\t').strip()
return result
class PricesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'cost', 'currency')
def get_title(self, obj):
if hasattr(obj, 'message') and obj.message:
message = obj.message
elif hasattr(obj, 'shop') and obj.shop:
message = '%s (%s)' % (obj.shop.name, obj.shop.location)
else:
return u'%s%s' % (obj.cost, obj.currency)
return u'%s%s - %s' % (obj.cost, obj.currency, message)
def get_description(self, obj):
if obj.date:
return obj.date.strftime('%Y-%m-%d')
class Comparoob(ReplApplication):
APPNAME = 'comparoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Romain Bignon'
DESCRIPTION = "Console application to compare products."
SHORT_DESCRIPTION = "compare products"
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'prices': PricesFormatter,
'price': PriceFormatter,
}
COMMANDS_FORMATTERS = {'prices': 'prices',
'info': 'price',
}
CAPS = CapPriceComparison
@defaultcount(10)
def do_prices(self, pattern):
"""
prices [PATTERN]
Display prices for a product. If a pattern is supplied, do not prompt
what product to compare.
"""
products = {}
for product in self.do('search_products', pattern):
double = False
for prod in products.keys():
if product.name == prod:
double = True
products[product.name].append(product)
break
if not double:
products[product.name] = [product]
products_type = None
products_names = list(products.keys())
if len(products_names) == 0:
print('Error: no product found with this pattern', file=self.stderr)
return 1
elif len(products_names) == 1:
products_type = products_names[0]
else:
print('What product do you want to compare?')
for i, p in enumerate(products_names):
print(' %s%2d)%s %s' % (self.BOLD, i+1, self.NC, p))
r = int(self.ask(' Select a product', regexp='\d+'))
while products_type is None:
if r <= 0 or r > len(products):
print('Error: Please enter a valid ID')
continue
products_type = products_names[r-1]
self.change_path([u'prices'])
_products = self.get_object_list('iter_prices', products.get(products_type))
self._sort_display_products(_products)
def _sort_display_products(self, products):
if products:
self.start_format()
for price in sorted(products, key=self._get_price):
self.cached_format(price)
def bcall_errors_handler(self, errors, debugmsg='Use --debug option to print backtraces', ignore=()):
for backend, error, backtrace in errors.errors:
if isinstance(error, MoreResultsAvailable):
products = self.get_object_list()
self._sort_display_products(products)
ConsoleApplication.bcall_errors_handler(self, errors, debugmsg, ignore)
def _get_price(self, price):
return price.cost
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a product.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
price = self.get_object(_id, 'get_price')
if not price:
print('Price not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(price)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/cookboob/ 0000775 0000000 0000000 00000000000 13204006733 0026060 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/cookboob/__init__.py 0000664 0000000 0000000 00000001423 13204006733 0030171 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .cookboob import Cookboob
__all__ = ['Cookboob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/cookboob/cookboob.py 0000664 0000000 0000000 00000012472 13204006733 0030235 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import codecs
from weboob.capabilities.recipe import CapRecipe
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Cookboob']
class RecipeInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'title', 'preparation_time', 'ingredients', 'instructions')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'ID: %s\n' % obj.fullid
if not empty(obj.author):
result += 'Author: %s\n' % obj.author
if not empty(obj.preparation_time):
result += 'Preparation time: %smin\n' % obj.preparation_time
if not empty(obj.cooking_time):
result += 'Cooking time: %smin\n' % obj.cooking_time
if not empty(obj.nb_person):
nbstr = '-'.join(str(num) for num in obj.nb_person)
result += 'Amount of people: %s\n' % nbstr
result += '\n%sIngredients%s\n' % (self.BOLD, self.NC)
for i in obj.ingredients:
result += ' * %s\n' % i
result += '\n%sInstructions%s\n' % (self.BOLD, self.NC)
result += '%s\n' % obj.instructions
if not empty(obj.comments):
result += '\n%sComments%s\n' % (self.BOLD, self.NC)
for c in obj.comments:
result += u' * %s\n' % c
return result
class RecipeListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'short_description', 'preparation_time')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = u''
if not empty(obj.preparation_time):
result += 'prep time: %smin' % obj.preparation_time
if not empty(obj.short_description):
result += 'description: %s\n' % obj.short_description
return result.strip()
class Cookboob(ReplApplication):
APPNAME = 'cookboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Julien Veyssier'
DESCRIPTION = "Console application allowing to search for recipes on various websites."
SHORT_DESCRIPTION = "search and consult recipes"
CAPS = CapRecipe
EXTRA_FORMATTERS = {'recipe_list': RecipeListFormatter,
'recipe_info': RecipeInfoFormatter
}
COMMANDS_FORMATTERS = {'search': 'recipe_list',
'info': 'recipe_info'
}
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, id):
"""
info ID
Get information about a recipe.
"""
recipe = self.get_object(id, 'get_recipe')
if not recipe:
print('Recipe not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(recipe)
def complete_export(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_export(self, line):
"""
export ID [FILENAME]
Export the recipe to a KRecipes XML file
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(id)
if dest is None:
dest = '%s.kreml' % _id
recipe = self.get_object(id, 'get_recipe')
if recipe:
xmlstring = recipe.toKrecipesXml(backend_name or None)
if dest == '-':
print(xmlstring)
else:
if not dest.endswith('.kreml'):
dest += '.kreml'
try:
with codecs.open(dest, 'w', 'utf-8') as f:
f.write(xmlstring)
except IOError as e:
print('Unable to write .kreml in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
print('Recipe "%s" not found' % id, file=self.stderr)
return 3
@defaultcount(10)
def do_search(self, pattern):
"""
search [PATTERN]
Search recipes.
"""
self.change_path([u'search'])
self.start_format(pattern=pattern)
for recipe in self.do('iter_recipes', pattern=pattern):
self.cached_format(recipe)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/flatboob/ 0000775 0000000 0000000 00000000000 13204006733 0026053 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/flatboob/__init__.py 0000664 0000000 0000000 00000000067 13204006733 0030167 0 ustar 00root root 0000000 0000000 from .flatboob import Flatboob
__all__ = ['Flatboob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/flatboob/flatboob.py 0000664 0000000 0000000 00000023130 13204006733 0030214 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.housing import CapHousing, Query
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.tools.config.yamlconfig import YamlConfig
__all__ = ['Flatboob']
class HousingFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'title', 'cost', 'currency', 'area', 'date', 'text')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'ID: %s\n' % obj.fullid
if hasattr(obj, 'url') and obj.url:
result += 'URL: %s\n' % obj.url
result += 'Cost: %s%s %s\n' % (obj.cost, obj.currency, obj.utilities)
area = u'%.2fm²' % (obj.area) if obj.area else u'%s' % obj.area
result += u'Area: %s\n' % area
if hasattr(obj, 'price_per_meter') and not empty(obj.price_per_meter):
result += u'Price per square meter: %.2f %s/m²\n' % (obj.price_per_meter, obj.currency)
if hasattr(obj, 'rooms') and not empty(obj.rooms):
result += u'Rooms: %d\n' % (obj.rooms)
if hasattr(obj, 'bedrooms') and not empty(obj.bedrooms):
result += u'Bedrooms: %d\n' % (obj.bedrooms)
if obj.date:
result += 'Date: %s\n' % obj.date.strftime('%Y-%m-%d')
result += 'Phone: %s\n' % obj.phone
if hasattr(obj, 'location') and obj.location:
result += 'Location: %s\n' % obj.location
if hasattr(obj, 'station') and obj.station:
result += 'Station: %s\n' % obj.station
if hasattr(obj, 'photos') and obj.photos:
result += '\n%sPhotos%s\n' % (self.BOLD, self.NC)
for photo in obj.photos:
result += ' * %s\n' % photo.url
result += '\n%sDescription%s\n' % (self.BOLD, self.NC)
result += obj.text
if hasattr(obj, 'details') and obj.details:
result += '\n\n%sDetails%s\n' % (self.BOLD, self.NC)
for key, value in obj.details.items():
result += ' %s: %s\n' % (key, value)
return result
class HousingListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'cost', 'text')
def get_title(self, obj):
return '%s%s %s - %s' % (obj.cost, obj.currency, obj.utilities, obj.title)
def get_description(self, obj):
result = u''
if hasattr(obj, 'date') and obj.date:
result += '%s - ' % obj.date.strftime('%Y-%m-%d')
result += obj.text
return result
class Flatboob(ReplApplication):
APPNAME = 'flatboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Romain Bignon'
DESCRIPTION = "Console application to search for housing."
SHORT_DESCRIPTION = "search for housing"
CAPS = CapHousing
CONFIG = {'queries': {}}
EXTRA_FORMATTERS = {'housing_list': HousingListFormatter,
'housing': HousingFormatter,
}
COMMANDS_FORMATTERS = {'search': 'housing_list',
'info': 'housing',
'load': 'housing_list'
}
def main(self, argv):
self.load_config(klass=YamlConfig)
return ReplApplication.main(self, argv)
@defaultcount(10)
def do_search(self, line):
"""
search
Search for housing. Parameters are interactively asked.
"""
pattern = 'notempty'
query = Query()
query.cities = []
while pattern:
if len(query.cities) > 0:
print('\n%sSelected cities:%s %s' % (self.BOLD, self.NC, ', '.join([c.name for c in query.cities])))
pattern = self.ask('Enter a city pattern (or empty to stop)', default='')
if not pattern:
break
cities = []
for city in self.weboob.do('search_city', pattern):
cities.append(city)
if len(cities) == 0:
print(' Not found!')
continue
if len(cities) == 1:
if city in query.cities:
query.cities.remove(city)
else:
query.cities.append(city)
continue
r = 'notempty'
while r != '':
for i, city in enumerate(cities):
print(' %s%2d)%s [%s] %s (%s)' % (self.BOLD, i+1, self.NC, 'x' if city in query.cities else ' ', city.name, city.backend))
r = self.ask(' Select cities (or empty to stop)', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(cities):
continue
city = cities[r-1]
if city in query.cities:
query.cities.remove(city)
else:
query.cities.append(city)
r = 'notempty'
while r != '':
for good in Query.HOUSE_TYPES.values:
print(' %s%2d)%s [%s] %s' % (self.BOLD,
Query.HOUSE_TYPES.index[good] + 1,
self.NC,
'x' if good in query.house_types else ' ', good))
r = self.ask(' Select type of house (or empty to stop)', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(Query.HOUSE_TYPES.values):
continue
value = Query.HOUSE_TYPES.values[r - 1]
if value in query.house_types:
query.house_types.remove(value)
else:
query.house_types.append(value)
_type = None
while _type not in [query.TYPE_RENT, query.TYPE_SALE, query.TYPE_SHARING]:
print(' %s%2d)%s %s' % (self.BOLD,
query.TYPE_RENT,
self.NC,
"Rent"))
print(' %s%2d)%s %s' % (self.BOLD,
query.TYPE_SALE,
self.NC,
"Sale"))
print(' %s%2d)%s %s' % (self.BOLD,
query.TYPE_SHARING,
self.NC,
"Sharing"))
_type = self.ask_int('Type of query')
query.type = _type
query.area_min = self.ask_int('Enter min area')
query.area_max = self.ask_int('Enter max area')
query.cost_min = self.ask_int('Enter min cost')
query.cost_max = self.ask_int('Enter max cost')
query.nb_rooms = self.ask_int('Enter number of rooms')
save_query = self.ask('Save query (y/n)', default='n')
if save_query.upper() == 'Y':
name = ''
while not name:
name = self.ask('Query name')
self.config.set('queries', name, query)
self.config.save()
self.complete_search(query)
def complete_search(self, query):
self.change_path([u'housings'])
self.start_format()
for housing in self.do('search_housings', query):
self.cached_format(housing)
def ask_int(self, txt):
r = self.ask(txt, default='', regexp='(\d+|)')
if r:
return int(r)
return None
@defaultcount(10)
def do_load(self, query_name):
"""
load [query name]
without query name : list loadable queries
with query name laod query
"""
queries = self.config.get('queries')
if not queries:
print('There is no saved queries', file=self.stderr)
return 2
if not query_name:
for name in queries.keys():
print(' %s* %s %s' % (self.BOLD,
self.NC,
name))
query_name = self.ask('Which one')
if query_name in queries:
self.complete_search(queries.get(query_name))
else:
print('Unknown query', file=self.stderr)
return 2
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a housing.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
housing = self.get_object(_id, 'get_housing')
if not housing:
print('Housing not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(housing)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/galleroob/ 0000775 0000000 0000000 00000000000 13204006733 0026231 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/galleroob/__init__.py 0000664 0000000 0000000 00000001434 13204006733 0030344 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Noé Rubinstein
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .galleroob import Galleroob
__all__ = ['Galleroob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/galleroob/galleroob.py 0000664 0000000 0000000 00000011272 13204006733 0030554 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Noé Rubinstein
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
from re import search, sub
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.capabilities.base import empty
from weboob.capabilities.gallery import CapGallery, BaseGallery, BaseImage
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Galleroob']
class GalleryListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
s = obj.title
if hasattr(obj, 'cardinality') and not empty(obj.cardinality):
s += u' (%d pages)' % obj.cardinality
return s
def get_description(self, obj):
if hasattr(obj, 'description') and obj.description:
return obj.description
class Galleroob(ReplApplication):
APPNAME = 'galleroob'
VERSION = '1.4'
COPYRIGHT = u'Copyright(C) 2011-2014 Noé Rubinstein'
DESCRIPTION = 'galleroob browses and downloads web image galleries'
SHORT_DESCRIPTION = 'browse and download web image galleries'
CAPS = CapGallery
EXTRA_FORMATTERS = {'gallery_list': GalleryListFormatter}
COMMANDS_FORMATTERS = {'search': 'gallery_list', 'ls': 'gallery_list'}
COLLECTION_OBJECTS = (BaseGallery, BaseImage, )
def __init__(self, *args, **kwargs):
super(Galleroob, self).__init__(*args, **kwargs)
@defaultcount(10)
def do_search(self, pattern):
"""
search PATTERN
List galleries matching a PATTERN.
"""
if not pattern:
print('This command takes an argument: %s' % self.get_command_help('search', short=True), file=self.stderr)
return 2
self.start_format(pattern=pattern)
for gallery in self.do('search_galleries', pattern=pattern):
self.cached_format(gallery)
def do_download(self, line):
"""
download ID [FIRST [FOLDER]]
Download a gallery.
Begins at page FIRST (default: 0) and saves to FOLDER (default: title)
"""
_id, first, dest = self.parse_command_args(line, 3, 1)
if first is None:
first = 0
else:
first = int(first)
gallery = None
_id, backend = self.parse_id(_id)
for result in self.do('get_gallery', _id, backends=backend):
if result:
gallery = result
if not gallery:
print('Gallery not found: %s' % _id, file=self.stderr)
return 3
self.weboob[backend].fillobj(gallery, ('title',))
if dest is None:
dest = sub('/', ' ', gallery.title)
print("Downloading to %s" % dest)
try:
os.mkdir(dest)
except OSError:
pass # ignore error on existing directory
os.chdir(dest) # fail here if dest couldn't be created
i = 0
for img in self.weboob[backend].iter_gallery_images(gallery):
i += 1
if i < first:
continue
self.weboob[backend].fillobj(img, ('url', 'data'))
if img.data is None:
self.weboob[backend].fillobj(img, ('url', 'data'))
if img.data is None:
print("Couldn't get page %d, exiting" % i, file=self.stderr)
break
ext = search(r"\.([^\.]{1,5})$", img.url)
if ext:
ext = ext.group(1)
else:
ext = "jpg"
name = '%03d.%s' % (i, ext)
print('Writing file %s' % name)
with open(name, 'wb') as f:
f.write(img.data)
os.chdir(os.path.pardir)
def do_info(self, line):
"""
info ID
Get information about a gallery.
"""
_id, = self.parse_command_args(line, 1, 1)
gallery = self.get_object(_id, 'get_gallery')
if not gallery:
print('Gallery not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(gallery)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/geolooc/ 0000775 0000000 0000000 00000000000 13204006733 0025712 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/geolooc/__init__.py 0000664 0000000 0000000 00000001424 13204006733 0030024 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .geolooc import Geolooc
__all__ = ['Geolooc']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/geolooc/geolooc.py 0000664 0000000 0000000 00000003130 13204006733 0027710 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.geolocip import CapGeolocIp
from weboob.tools.application.repl import ReplApplication
__all__ = ['Geolooc']
class Geolooc(ReplApplication):
APPNAME = 'geolooc'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to geolocalize IP addresses."
SHORT_DESCRIPTION = "geolocalize IP addresses"
CAPS = CapGeolocIp
def main(self, argv):
if len(argv) < 2:
print('Syntax: %s ipaddr' % argv[0], file=self.stderr)
return 2
for location in self.do('get_location', argv[1]):
if location.lt and location.lg:
location.osmlink = u'http://www.openstreetmap.org/?mlat=%s&mlon=%s#map=13/%s/%s' % (location.lt, location.lg, location.lt, location.lg)
self.format(location)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/handjoob/ 0000775 0000000 0000000 00000000000 13204006733 0026047 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/handjoob/__init__.py 0000664 0000000 0000000 00000001415 13204006733 0030161 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .handjoob import Handjoob
__all__ = ['Handjoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/handjoob/handjoob.py 0000664 0000000 0000000 00000011546 13204006733 0030214 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.job import CapJob
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Handjoob']
class JobAdvertFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'url', 'publication_date', 'title')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'url: %s\n' % obj.url
if hasattr(obj, 'publication_date') and obj.publication_date:
result += 'Publication date : %s\n' % obj.publication_date.strftime('%Y-%m-%d')
if hasattr(obj, 'place') and obj.place:
result += 'Location: %s\n' % obj.place
if hasattr(obj, 'society_name') and obj.society_name:
result += 'Society : %s\n' % obj.society_name
if hasattr(obj, 'job_name') and obj.job_name:
result += 'Job name : %s\n' % obj.job_name
if hasattr(obj, 'contract_type') and obj.contract_type:
result += 'Contract : %s\n' % obj.contract_type
if hasattr(obj, 'pay') and obj.pay:
result += 'Pay : %s\n' % obj.pay
if hasattr(obj, 'formation') and obj.formation:
result += 'Formation : %s\n' % obj.formation
if hasattr(obj, 'experience') and obj.experience:
result += 'Experience : %s\n' % obj.experience
if hasattr(obj, 'description') and obj.description:
result += 'Description : %s\n' % obj.description
return result
class JobAdvertListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
return '%s' % (obj.title)
def get_description(self, obj):
result = u''
if hasattr(obj, 'publication_date') and obj.publication_date:
result += '\tPublication date : %s\n' % obj.publication_date.strftime('%Y-%m-%d')
if hasattr(obj, 'place') and obj.place:
result += '\tLocation: %s\n' % obj.place
if hasattr(obj, 'society_name') and obj.society_name:
result += '\tSociety : %s\n' % obj.society_name
if hasattr(obj, 'contract_type') and obj.contract_type:
result += '\tContract : %s\n' % obj.contract_type
return result.strip('\n\t')
class Handjoob(ReplApplication):
APPNAME = 'handjoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Bezleputh'
DESCRIPTION = "Console application to search for a job."
SHORT_DESCRIPTION = "search for a job"
CAPS = CapJob
EXTRA_FORMATTERS = {'job_advert_list': JobAdvertListFormatter,
'job_advert': JobAdvertFormatter,
}
COMMANDS_FORMATTERS = {'search': 'job_advert_list',
'ls': 'job_advert_list',
'info': 'job_advert',
}
@defaultcount(10)
def do_search(self, pattern):
"""
search PATTERN
Search for an advert matching a PATTERN.
"""
self.change_path([u'search'])
self.start_format(pattern=pattern)
for job_advert in self.do('search_job', pattern):
self.cached_format(job_advert)
@defaultcount(10)
def do_ls(self, line):
"""
advanced search
Search for an advert matching to advanced filters.
"""
self.change_path([u'advanced'])
for job_advert in self.do('advanced_search_job'):
self.cached_format(job_advert)
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about an advert.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
job_advert = self.get_object(_id, 'get_job_advert')
if not job_advert:
print('Job advert not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(job_advert)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/havedate/ 0000775 0000000 0000000 00000000000 13204006733 0026044 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/havedate/__init__.py 0000664 0000000 0000000 00000001427 13204006733 0030161 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .havedate import HaveDate
__all__ = ['HaveDate']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/havedate/havedate.py 0000664 0000000 0000000 00000024162 13204006733 0030204 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from copy import copy
from weboob.core import CallErrors
from weboob.tools.application.repl import ReplApplication
from weboob.applications.boobmsg import Boobmsg
from weboob.capabilities.dating import CapDating, OptimizationNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['HaveDate']
class EventListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('date', 'type')
def get_title(self, event):
s = u'(%s) %s' % (event.date, event.type)
if hasattr(event, 'contact') and event.contact:
s += u' — %s (%s)' % (event.contact.name, event.contact.id)
return s
def get_description(self, event):
if hasattr(event, 'message'):
return event.message
class HaveDate(Boobmsg):
APPNAME = 'havedate'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to interact with various dating websites " \
"and to optimize seduction algorithmically."
SHORT_DESCRIPTION = "interact with dating websites"
STORAGE_FILENAME = 'dating.storage'
STORAGE = {'optims': {}}
CAPS = CapDating
EXTRA_FORMATTERS = copy(Boobmsg.EXTRA_FORMATTERS)
EXTRA_FORMATTERS['events'] = EventListFormatter
COMMANDS_FORMATTERS = copy(Boobmsg.COMMANDS_FORMATTERS)
COMMANDS_FORMATTERS['optim'] = 'table'
COMMANDS_FORMATTERS['events'] = 'events'
def load_default_backends(self):
self.load_backends(CapDating, storage=self.create_storage(self.STORAGE_FILENAME))
def main(self, argv):
self.load_config()
try:
self.do('init_optimizations').wait()
except CallErrors as e:
self.bcall_errors_handler(e)
optimizations = self.storage.get('optims')
for optim, backends in optimizations.items():
self.optims('start', backends, optim, store=False)
return ReplApplication.main(self, argv)
def do_query(self, id):
"""
query ID
Send a query to someone.
"""
_id, backend_name = self.parse_id(id, unique_backend=True)
for query in self.do('send_query', _id, backends=backend_name):
print('%s' % query.message)
def edit_optims(self, backend_names, optims_names, stop=False):
if optims_names is None:
print('Error: missing parameters.', file=self.stderr)
return 2
for optim_name in optims_names.split():
backends_optims = {}
for optim in self.do('get_optimization', optim_name, backends=backend_names):
if optim:
backends_optims[optim.backend] = optim
for backend_name, optim in backends_optims.items():
if len(optim.CONFIG) == 0:
print('%s.%s does not require configuration.' % (backend_name, optim_name))
continue
was_running = optim.is_running()
if stop and was_running:
print('Stopping %s: %s' % (optim_name, backend_name))
optim.stop()
params = optim.get_config()
if params is None:
params = {}
print('Configuration of %s.%s' % (backend_name, optim_name))
print('-----------------%s-%s' % ('-' * len(backend_name), '-' * len(optim_name)))
for key, value in optim.CONFIG.items():
params[key] = self.ask(value, default=params[key] if (key in params) else value.default)
optim.set_config(params)
if stop and was_running:
print('Starting %s: %s' % (optim_name, backend_name))
optim.start()
def optims(self, function, backend_names, optims, store=True):
if optims is None:
print('Error: missing parameters.', file=self.stderr)
return 2
for optim_name in optims.split():
try:
if store:
storage_optim = set(self.storage.get('optims', optim_name, default=[]))
self.stdout.write('%sing %s:' % (function.capitalize(), optim_name))
for optim in self.do('get_optimization', optim_name, backends=backend_names):
if optim:
# It's useless to start a started optim, or to stop a stopped one.
if (function == 'start' and optim.is_running()) or \
(function == 'stop' and not optim.is_running()):
continue
# Optim is not configured and would be, ask user to do it.
if function == 'start' and len(optim.CONFIG) > 0 and optim.get_config() is None:
self.edit_optims(optim.backend, optim_name)
ret = getattr(optim, function)()
self.stdout.write(' ' + optim.backend)
if not ret:
self.stdout.write('(failed)')
self.stdout.flush()
if store:
if function == 'start' and ret:
storage_optim.add(optim.backend)
elif function == 'stop':
try:
storage_optim.remove(optim.backend)
except KeyError:
pass
self.stdout.write('.\n')
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, OptimizationNotFound):
self.logger.error(u'Error(%s): Optimization "%s" not found' % (backend.name, optim_name))
else:
self.bcall_error_handler(backend, error, backtrace)
if store:
if len(storage_optim) > 0:
self.storage.set('optims', optim_name, list(storage_optim))
else:
self.storage.delete('optims', optim_name)
if store:
self.storage.save()
def complete_optim(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return ['list', 'start', 'stop', 'edit']
elif len(args) == 3:
return [backend.name for backend in self.enabled_backends]
elif len(args) >= 4:
if args[2] == '*':
backend = None
else:
backend = args[2]
optims = set()
for optim in self.do('iter_optimizations', backends=backend):
optims.add(optim.id)
return sorted(optims - set(args[3:]))
def do_optim(self, line):
"""
optim [list | start | edit | stop] BACKEND [OPTIM1 [OPTIM2 ...]]
All dating backends offer optimization services. This command can be
manage them.
Use * us BACKEND value to apply command to all backends.
Commands:
* list list all available optimizations of a backend
* start start optimization services on a backend
* edit configure an optimization service for a backend
* stop stop optimization services on a backend
"""
cmd, backend_name, optims_names = self.parse_command_args(line, 3)
if backend_name == '*':
backend_name = None
elif backend_name is not None and backend_name not in [b.name for b in self.enabled_backends]:
print('Error: No such backend "%s"' % backend_name, file=self.stderr)
return 1
if cmd == 'start':
return self.optims('start', backend_name, optims_names)
if cmd == 'stop':
return self.optims('stop', backend_name, optims_names)
if cmd == 'edit':
self.edit_optims(backend_name, optims_names, stop=True)
return
if cmd == 'list' or cmd is None:
if optims_names is not None:
optims_names = optims_names.split()
optims = {}
backends = set()
for optim in self.do('iter_optimizations', backends=backend_name):
if optims_names is not None and optim.id not in optims_names:
continue
if optim.is_running():
status = 'RUNNING'
else:
status = '-------'
if optim.id not in optims:
optims[optim.id] = {optim.backend: status}
else:
optims[optim.id][optim.backend] = status
backends.add(optim.backend)
backends = sorted(backends)
for name, backends_status in optims.items():
line = [('name', name)]
for b in backends:
try:
status = backends_status[b]
except KeyError:
status = ''
line.append((b, status))
self.format(tuple(line))
return
print("No such command '%s'" % cmd, file=self.stderr)
return 1
def do_events(self, line):
"""
events
Display dating events.
"""
self.change_path([u'events'])
self.start_format()
for event in self.do('iter_events'):
self.cached_format(event)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/masstransit/ 0000775 0000000 0000000 00000000000 13204006733 0026633 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/masstransit/__init__.py 0000664 0000000 0000000 00000001440 13204006733 0030743 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .masstransit import Masstransit
__all__ = ['Masstransit']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/masstransit/masstransit.py 0000664 0000000 0000000 00000024352 13204006733 0031563 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Julien Hébert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.travel import CapTravel
from weboob.tools.application.base import Application
from logging import warning
import gtk
class FakeConic(object):
STATUS_CONNECTED = None
STATUS_DISCONNECTED = None
CONNECT_FLAG_NONE = None
def Connection(self):
raise NotImplementedError()
try:
import hildon
except ImportError:
toolkit = gtk
else:
toolkit = hildon
try:
import conic
except ImportError:
warning("conic is not found")
conic = FakeConic()
from logging import debug
__all__ = ['Masstransit']
class MasstransitHildon(object):
"hildon interface"
def connect_event(self, connection, event=None, c=None, d=None):
debug("DBUS-DEBUG a: %s, b:%s, c:%s,d: %s" % (connection, event, c, d))
status = event.get_status()
if status == conic.STATUS_CONNECTED:
self.connected = True
if not self.touch_selector_entry_filled:
debug("connected, now fill")
self.fill_touch_selector_entry()
if self.refresh_in_progress:
self.refresh()
elif status == conic.STATUS_DISCONNECTED:
self.connected = False
def __init__(self, weboob):
self.touch_selector_entry_filled = False
self.refresh_in_progress = False
self.connected = False
self.weboob = weboob
try:
self.connection = conic.Connection()
self.connection.connect("connection-event", self.connect_event)
self.connection.set_property("automatic-connection-events", True)
self.connection.request_connection(conic.CONNECT_FLAG_NONE)
except NotImplementedError:
pass
horizontal_box = gtk.HBox()
self.main_window = toolkit.Window()
try:
self.refresh_button = toolkit.Button(
gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
hildon.BUTTON_ARRANGEMENT_HORIZONTAL,
"Actualiser"
)
self.retour_button = hildon.Button(
gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
hildon.BUTTON_ARRANGEMENT_HORIZONTAL,
"Retour"
)
self.combo_source = hildon.TouchSelectorEntry(text=True)
self.combo_dest = hildon.TouchSelectorEntry(text=True)
self.picker_button_source = hildon.PickerButton(
gtk.HILDON_SIZE_AUTO,
hildon.BUTTON_ARRANGEMENT_VERTICAL)
self.picker_button_dest = hildon.PickerButton(
gtk.HILDON_SIZE_AUTO,
hildon.BUTTON_ARRANGEMENT_VERTICAL
)
self.picker_button_source.set_sensitive(False)
self.picker_button_dest.set_sensitive(False)
self.picker_button_source.set_title("Gare de Depart")
self.picker_button_dest.set_title("Gare d'arrivee")
self.picker_button_source.set_selector(self.combo_source)
self.picker_button_dest.set_selector(self.combo_dest)
horizontal_box.pack_start(self.picker_button_source)
horizontal_box.pack_start(self.picker_button_dest)
except AttributeError:
self.refresh_button = gtk.Button("Actualiser")
self.retour_button = gtk.Button("Retour")
self.combo_source = gtk.combo_box_entry_new_text()
self.combo_dest = gtk.combo_box_entry_new_text()
horizontal_box.pack_start(self.combo_source)
horizontal_box.pack_start(self.combo_dest)
self.main_window.set_title("Horaires des Prochains Trains")
self.main_window.connect("destroy", self.on_main_window_destroy)
self.refresh_button.connect("clicked", self.on_refresh_button_clicked)
self.retour_button.set_sensitive(False)
self.retour_button.connect("clicked", self.on_retour_button_clicked)
self.treestore = gtk.TreeStore(str, str, str, str, str)
treeview = gtk.TreeView(self.treestore)
treeview.append_column(
gtk.TreeViewColumn(
'Train',
gtk.CellRendererText(),
text=0
))
treeview.append_column(
gtk.TreeViewColumn(
'Horaire',
gtk.CellRendererText(),
text=1
))
treeview.append_column(
gtk.TreeViewColumn(
'Destination',
gtk.CellRendererText(),
text=2
))
treeview.append_column(
gtk.TreeViewColumn(
'Voie',
gtk.CellRendererText(),
text=3
))
treeview.append_column(
gtk.TreeViewColumn(
'Information',
gtk.CellRendererText(),
text=4
))
vertical_box = gtk.VBox()
vertical_box.pack_start(horizontal_box)
horizontal_box.pack_start(self.retour_button)
vertical_box.pack_start(treeview)
vertical_box.pack_start(self.refresh_button)
self.main_window.add(vertical_box)
self.main_window.show_all()
self.fill_touch_selector_entry()
if toolkit != gtk:
self.picker_button_source.connect("value-changed",
self.check_station_input,
self.picker_button_source)
self.picker_button_dest.connect("value-changed",
self.check_station_input,
self.picker_button_dest)
def fill_touch_selector_entry(self):
liste = []
for backend in self.weboob.iter_backends():
for station in backend.iter_station_search(""):
liste.append(station.name.capitalize())
liste.sort()
for station in liste:
self.combo_source.append_text(station)
self.combo_dest.append_text(station)
self.touch_selector_entry_filled = True
if toolkit != gtk:
self.picker_button_source.set_sensitive(True)
def on_main_window_destroy(self, widget):
"exit application at the window close"
gtk.main_quit()
def on_main_window_show(self, param):
self.fill_touch_selector_entry()
def on_retour_button_clicked(self, widget):
"the button is clicked"
debug("on_retour_button_clicked")
self.refresh_in_progress = True
col_source = self.combo_source.get_active(0)
col_dest = self.combo_dest.get_active(0)
self.combo_source.set_active(0, col_dest)
self.combo_dest.set_active(0, col_source)
self.refresh()
def on_refresh_button_clicked(self, widget):
"the refresh button is clicked"
debug("on_refresh_button_clicked")
self.refresh_in_progress = True
try:
self.connection.request_connection(conic.CONNECT_FLAG_NONE)
except AttributeError:
if isinstance(conic, FakeConic):
self.refresh()
else:
raise
def check_station_input(self, widget, user_data):
if self.combo_source.get_current_text() is None:
self.picker_button_dest.set_sensitive(False)
self.refresh_button.set_sensitive(False)
self.retour_button.set_sensitive(False)
else:
self.picker_button_dest.set_sensitive(True)
if self.combo_dest.get_current_text() is None:
self.refresh_button.set_sensitive(False)
self.retour_button.set_sensitive(False)
else:
self.refresh_button.set_sensitive(True)
self.retour_button.set_sensitive(True)
def refresh(self):
"update departures"
banner = hildon.hildon_banner_show_information(self.main_window, "", "Chargement en cours")
banner.set_timeout(10000)
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, 1)
self.treestore.clear()
try:
source_text = self.combo_source.get_current_text()
dest_text = self.combo_dest.get_current_text()
except AttributeError:
source_text = self.combo_source.child.get_text()
dest_text = self.combo_dest.child.get_text()
for backend in self.weboob.iter_backends():
for station in backend.iter_station_search(source_text):
for arrival in \
backend.iter_station_search(dest_text):
for departure in \
backend.iter_station_departures(station.id, arrival.id):
self.treestore.append(None,
[departure.type,
departure.time,
departure.arrival_station,
departure.plateform,
departure.information])
self.refresh_in_progress = False
banner.set_timeout(1)
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, 0)
class Masstransit(Application):
"Application Class"
APPNAME = 'masstransit'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Julien Hébert'
DESCRIPTION = "Maemo application allowing to search for train stations and get departure times."
SHORT_DESCRIPTION = "search for train stations and departures"
def main(self, argv):
self.load_backends(CapTravel)
MasstransitHildon(self.weboob)
gtk.main()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/monboob/ 0000775 0000000 0000000 00000000000 13204006733 0025716 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/monboob/__init__.py 0000664 0000000 0000000 00000001424 13204006733 0030030 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .monboob import Monboob
__all__ = ['Monboob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/monboob/monboob.py 0000664 0000000 0000000 00000032625 13204006733 0027733 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2009-2011 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from email.mime.text import MIMEText
from smtplib import SMTP
from email.Header import Header, decode_header
from email.Utils import parseaddr, formataddr, formatdate
from email import message_from_file, message_from_string
from smtpd import SMTPServer
import time
import re
import logging
import asyncore
import subprocess
import socket
from weboob.core import Weboob, CallErrors
from weboob.core.scheduler import Scheduler
from weboob.capabilities.messages import CapMessages, CapMessagesPost, Thread, Message
from weboob.tools.application.repl import ReplApplication
from weboob.tools.compat import unicode
from weboob.tools.date import utc2local
from weboob.tools.html import html2text
from weboob.tools.misc import get_backtrace, to_unicode
__all__ = ['Monboob']
class FakeSMTPD(SMTPServer):
def __init__(self, app, bindaddr, port):
SMTPServer.__init__(self, (bindaddr, port), None)
self.app = app
def process_message(self, peer, mailfrom, rcpttos, data):
msg = message_from_string(data)
self.app.process_incoming_mail(msg)
class MonboobScheduler(Scheduler):
def __init__(self, app):
super(MonboobScheduler, self).__init__()
self.app = app
def run(self):
if self.app.options.smtpd:
if ':' in self.app.options.smtpd:
host, port = self.app.options.smtpd.split(':', 1)
else:
host = '127.0.0.1'
port = self.app.options.smtpd
try:
FakeSMTPD(self.app, host, int(port))
except socket.error as e:
self.logger.error('Unable to start the SMTP daemon: %s' % e)
return False
# XXX Fuck, we shouldn't copy this piece of code from
# weboob.scheduler.Scheduler.run().
try:
while True:
self.stop_event.wait(0.1)
if self.app.options.smtpd:
asyncore.loop(timeout=0.1, count=1)
except KeyboardInterrupt:
self._wait_to_stop()
raise
else:
self._wait_to_stop()
return True
class Monboob(ReplApplication):
APPNAME = 'monboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = 'Daemon allowing to regularly check for new messages on various websites, ' \
'and send an email for each message, and post a reply to a message on a website.'
SHORT_DESCRIPTION = "daemon to send and check messages"
CONFIG = {'interval': 300,
'domain': 'weboob.example.org',
'recipient': 'weboob@example.org',
'smtp': 'localhost',
'pipe': '',
'html': 0}
CAPS = CapMessages
DISABLE_REPL = True
def add_application_options(self, group):
group.add_option('-S', '--smtpd', help='run a fake smtpd server and set the port')
def create_weboob(self):
return Weboob(scheduler=MonboobScheduler(self))
def load_default_backends(self):
self.load_backends(CapMessages, storage=self.create_storage())
def main(self, argv):
self.load_config()
try:
self.config.set('interval', int(self.config.get('interval')))
if self.config.get('interval') < 1:
raise ValueError()
except ValueError:
print('Configuration error: interval must be an integer >0.', file=self.stderr)
return 1
try:
self.config.set('html', int(self.config.get('html')))
if self.config.get('html') not in (0, 1):
raise ValueError()
except ValueError:
print('Configuration error: html must be 0 or 1.', file=self.stderr)
return 2
return ReplApplication.main(self, argv)
def get_email_address_ident(self, msg, header):
s = msg.get(header)
if not s:
return None
m = re.match('.*<([^@]*)@(.*)>', s)
if m:
return m.group(1)
else:
try:
return s.split('@')[0]
except IndexError:
return s
def do_post(self, line):
"""
post
Pipe with a mail to post message.
"""
msg = message_from_file(self.stdin)
return self.process_incoming_mail(msg)
def process_incoming_mail(self, msg):
to = self.get_email_address_ident(msg, 'To')
sender = msg.get('From')
reply_to = self.get_email_address_ident(msg, 'In-Reply-To')
title = msg.get('Subject')
if title:
new_title = u''
for part in decode_header(title):
if part[1]:
new_title += unicode(part[0], part[1])
else:
new_title += unicode(part[0])
title = new_title
content = u''
for part in msg.walk():
if part.get_content_type() == 'text/plain':
s = part.get_payload(decode=True)
charsets = part.get_charsets() + msg.get_charsets()
for charset in charsets:
try:
if charset is not None:
content += unicode(s, charset)
else:
content += unicode(s)
except UnicodeError as e:
self.logger.warning('Unicode error: %s' % e)
continue
except Exception as e:
self.logger.exception(e)
continue
else:
break
if len(content) == 0:
print('Unable to send an empty message', file=self.stderr)
return 1
# remove signature
content = content.split(u'\n-- \n')[0]
parent_id = None
if reply_to is None:
# This is a new message
if '.' in to:
backend_name, thread_id = to.split('.', 1)
else:
backend_name = to
thread_id = None
else:
# This is a reply
try:
backend_name, id = reply_to.split('.', 1)
thread_id, parent_id = id.rsplit('.', 1)
except ValueError:
print('In-Reply-To header might be in form ', file=self.stderr)
return 1
# Default use the To header field to know the backend to use.
if to and backend_name != to:
backend_name = to
try:
backend = self.weboob.backend_instances[backend_name]
except KeyError:
print('Backend %s not found' % backend_name, file=self.stderr)
return 1
if not backend.has_caps(CapMessagesPost):
print('The backend %s does not implement CapMessagesPost' % backend_name, file=self.stderr)
return 1
thread = Thread(thread_id)
message = Message(thread,
0,
title=title,
sender=sender,
receivers=[to],
parent=Message(thread, parent_id) if parent_id else None,
content=content)
try:
backend.post_message(message)
except Exception as e:
content = u'Unable to send message to %s:\n' % thread_id
content += u'\n\t%s\n' % to_unicode(e)
if logging.root.level <= logging.DEBUG:
content += u'\n%s\n' % to_unicode(get_backtrace(e))
self.send_email(backend.name, Message(thread,
0,
title='Unable to send message',
sender='Monboob',
parent=Message(thread, parent_id) if parent_id else None,
content=content))
def do_run(self, line):
"""
run
Run the fetching daemon.
"""
self.weboob.repeat(self.config.get('interval'), self.process)
self.weboob.loop()
def do_once(self, line):
"""
once
Send mails only once, then exit.
"""
return self.process()
def process(self):
try:
for message in self.weboob.do('iter_unread_messages'):
if self.send_email(message.backend, message):
self.weboob[message.backend].set_message_read(message)
except CallErrors as e:
self.bcall_errors_handler(e)
def send_email(self, backend_name, mail):
domain = self.config.get('domain')
recipient = self.config.get('recipient')
parent_message = mail.parent
references = []
while parent_message:
references.append(u'<%s.%s@%s>' % (backend_name, mail.parent.full_id, domain))
parent_message = parent_message.parent
subject = mail.title
sender = u'"%s" <%s@%s>' % (mail.sender.replace('"', '""') if mail.sender else '',
backend_name, domain)
# assume that .date is an UTC datetime
date = formatdate(time.mktime(utc2local(mail.date).timetuple()), localtime=True)
msg_id = u'<%s.%s@%s>' % (backend_name, mail.full_id, domain)
if self.config.get('html') and mail.flags & mail.IS_HTML:
body = mail.content
content_type = 'html'
else:
if mail.flags & mail.IS_HTML:
body = html2text(mail.content)
else:
body = mail.content
content_type = 'plain'
if body is None:
body = ''
if mail.signature:
if self.config.get('html') and mail.flags & mail.IS_HTML:
body += u'
-- %s
' % mail.signature
else:
body += u'\n\n-- \n'
if mail.flags & mail.IS_HTML:
body += html2text(mail.signature)
else:
body += mail.signature
# Header class is smart enough to try US-ASCII, then the charset we
# provide, then fall back to UTF-8.
header_charset = 'ISO-8859-1'
# We must choose the body charset manually
for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8':
try:
body.encode(body_charset)
except UnicodeError:
pass
else:
break
# Split real name (which is optional) and email address parts
sender_name, sender_addr = parseaddr(sender)
recipient_name, recipient_addr = parseaddr(recipient)
# We must always pass Unicode strings to Header, otherwise it will
# use RFC 2047 encoding even on plain ASCII strings.
sender_name = str(Header(unicode(sender_name), header_charset))
recipient_name = str(Header(unicode(recipient_name), header_charset))
# Make sure email addresses do not contain non-ASCII characters
sender_addr = sender_addr.encode('ascii')
recipient_addr = recipient_addr.encode('ascii')
# Create the message ('plain' stands for Content-Type: text/plain)
msg = MIMEText(body.encode(body_charset), content_type, body_charset)
msg['From'] = formataddr((sender_name, sender_addr))
msg['To'] = formataddr((recipient_name, recipient_addr))
msg['Subject'] = Header(unicode(subject), header_charset)
msg['Message-Id'] = msg_id
msg['Date'] = date
if references:
msg['In-Reply-To'] = references[0]
msg['References'] = u" ".join(reversed(references))
self.logger.info('Send mail from <%s> to <%s>' % (sender, recipient))
if len(self.config.get('pipe')) > 0:
p = subprocess.Popen(self.config.get('pipe'),
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p.stdin.write(msg.as_string())
p.stdin.close()
if p.wait() != 0:
self.logger.error('Unable to deliver mail: %s' % p.stdout.read().strip())
return False
else:
# Send the message via SMTP to localhost:25
try:
smtp = SMTP(self.config.get('smtp'))
smtp.sendmail(sender, recipient, msg.as_string())
except Exception as e:
self.logger.error('Unable to deliver mail: %s' % e)
return False
else:
smtp.quit()
return True
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/parceloob/ 0000775 0000000 0000000 00000000000 13204006733 0026231 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/parceloob/__init__.py 0000664 0000000 0000000 00000001425 13204006733 0030344 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .parceloob import Parceloob
__all__ = ['Parceloob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/parceloob/parceloob.py 0000664 0000000 0000000 00000015662 13204006733 0030563 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.base import empty
from weboob.capabilities.parcel import CapParcel, Parcel, ParcelNotFound
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Parceloob']
STATUS = {Parcel.STATUS_PLANNED: ('PLANNED', 'red'),
Parcel.STATUS_IN_TRANSIT: ('IN TRANSIT', 'yellow'),
Parcel.STATUS_ARRIVED: ('ARRIVED', 'green'),
Parcel.STATUS_UNKNOWN: ('', 'white'),
}
def get_backend_name(backend):
return backend.name
class HistoryFormatter(IFormatter):
MANDATORY_FIELDS = ()
def format_obj(self, obj, alias):
if isinstance(obj, Parcel):
result = u'Parcel %s (%s)\n' % (self.colored(obj.id, 'red', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
result += u'%sArrival:%s %s\n' % (self.BOLD, self.NC, obj.arrival)
status, status_color = STATUS[obj.status]
result += u'%sStatus:%s %s\n' % (self.BOLD, self.NC, self.colored(status, status_color))
result += u'%sInfo:%s %s\n\n' % (self.BOLD, self.NC, obj.info)
result += u' Date Location Activity \n'
result += u'---------------------+-----------------+---------------------------------------------------'
return result
return ' %s %s %s' % (self.colored('%-19s' % obj.date, 'blue'),
self.colored('%-17s' % (obj.location or ''), 'magenta'),
self.colored(obj.activity or '', 'yellow'))
class StatusFormatter(IFormatter):
MANDATORY_FIELDS = ('id',)
def format_obj(self, obj, alias):
if alias is not None:
id = '%s (%s)' % (self.colored('%3s' % ('#' + alias), 'red', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
clean = '#%s (%s)' % (alias, obj.backend)
if len(clean) < 15:
id += (' ' * (15 - len(clean)))
else:
id = self.colored('%30s' % obj.fullid, 'red', 'bold')
status, status_color = STATUS[obj.status]
arrival = obj.arrival.strftime('%Y-%m-%d') if not empty(obj.arrival) else ''
result = u'%s %s %s %s %s' % (id, self.colored(u'—', 'cyan'),
self.colored('%-10s' % status, status_color),
self.colored('%-10s' % arrival, 'blue'),
self.colored('%-20s' % obj.info, 'yellow'))
return result
class Parceloob(ReplApplication):
APPNAME = 'parceloob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Romain Bignon'
CAPS = CapParcel
DESCRIPTION = "Console application to track your parcels."
SHORT_DESCRIPTION = "manage your parcels"
EXTRA_FORMATTERS = {'status': StatusFormatter,
'history': HistoryFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'status': 'status',
'info': 'history',
}
STORAGE = {'tracking': []}
def do_track(self, line):
"""
track ID
Track a parcel.
"""
parcel = self.get_object(line, 'get_parcel_tracking')
if not parcel:
print('Error: the parcel "%s" is not found' % line, file=self.stderr)
return 2
parcels = set(self.storage.get('tracking', default=[]))
parcels.add(parcel.fullid)
self.storage.set('tracking', list(parcels))
self.storage.save()
print('Parcel "%s" has been tracked.' % parcel.fullid)
def do_untrack(self, line):
"""
untrack ID
Stop tracking a parcel.
"""
removed = False
# Always try to first remove the parcel, the untrack should always success
parcels = set(self.storage.get('tracking', default=[]))
try:
parcels.remove(line)
removed = True
except KeyError:
pass
if not removed:
try:
parcel = self.get_object(line, 'get_parcel_tracking')
except ParcelNotFound:
parcel = False
if not parcel:
print('Error: the parcel "%s" is not found. Did you provide the full id@backend parameter?' % line, file=self.stderr)
return 2
try:
parcels.remove(parcel.fullid)
except KeyError:
print("Error: parcel \"%s\" wasn't tracked" % parcel.fullid, file=self.stderr)
return 2
self.storage.set('tracking', list(parcels))
self.storage.save()
if removed:
print("Parcel \"%s\" isn't tracked anymore." % line)
else:
print("Parcel \"%s\" isn't tracked anymore." % parcel.fullid)
def do_status(self, line):
"""
status
Display status for all of the tracked parcels.
"""
backends = list(map(get_backend_name, self.enabled_backends))
self.start_format()
# XXX cleaning of cached objects may be by start_format()?
self.objects = []
for id in self.storage.get('tracking', default=[]):
# It should be safe to do it here, since all objects in storage
# are stored with the fullid
_id, backend_name = id.rsplit('@', 1)
# If the user use the -b or -e option, do not try to get
# the status of parcel of not loaded backends
if backend_name not in backends:
continue
p = self.get_object(id, 'get_parcel_tracking')
if p is None:
continue
self.cached_format(p)
def do_info(self, id):
"""
info ID
Get information about a parcel.
"""
parcel = self.get_object(id, 'get_parcel_tracking', [])
if not parcel:
print('Error: parcel not found', file=self.stderr)
return 2
self.start_format()
self.format(parcel)
for event in parcel.history:
self.format(event)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/pastoob/ 0000775 0000000 0000000 00000000000 13204006733 0025732 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/pastoob/__init__.py 0000664 0000000 0000000 00000001423 13204006733 0030043 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .pastoob import Pastoob
__all__ = ['Pastoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/pastoob/pastoob.py 0000664 0000000 0000000 00000017452 13204006733 0027764 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011-2014 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from base64 import b64decode, b64encode
import os
import codecs
import re
from random import choice
import sys
from weboob.capabilities.paste import CapPaste, PasteNotFound
from weboob.tools.application.repl import ReplApplication
__all__ = ['Pastoob']
class Pastoob(ReplApplication):
APPNAME = 'pastoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2011-YEAR Laurent Bachelier'
DESCRIPTION = "Console application allowing to post and get pastes from pastebins."
SHORT_DESCRIPTION = "post and get pastes from pastebins"
CAPS = CapPaste
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def do_info(self, line):
"""
info ID [ID2 [...]]
Get information about pastes.
"""
if not line:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
self.start_format()
for _id in line.split(' '):
paste = self.get_object(_id, 'get_paste', ['id', 'title', 'language', 'public', 'contents'])
if not paste:
print('Paste not found: %s' % _id, file=self.stderr)
self.format(paste)
def do_get(self, line):
"""
get ID
Get a paste contents.
"""
return self._get_op(line, binary=False, command='get')
def do_get_bin(self, line):
"""
get_bin ID
Get a paste contents.
File will be downloaded from binary services.
"""
return self._get_op(line, binary=True, command='get_bin')
def _get_op(self, _id, binary, command='get'):
if not _id:
print('This command takes an argument: %s' % self.get_command_help(command, short=True), file=self.stderr)
return 2
try:
paste = self.get_object(_id, 'get_paste', ['contents'])
except PasteNotFound:
print('Paste not found: %s' % _id, file=self.stderr)
return 3
if not paste:
print('Unable to handle paste: %s' % _id, file=self.stderr)
return 1
if binary:
if self.interactive:
if not self.ask('The console may become messed up. Are you sure you want to show a binary file on your terminal?', default=False):
print('Aborting.', file=self.stderr)
return 1
if sys.version_info.major >= 3:
output = self.stdout.buffer
else:
output = self.stdout.stream
output.write(b64decode(paste.contents))
else:
if sys.version_info.major < 3:
output = codecs.getwriter(self.encoding)(self.stdout)
else:
output = self.stdout
output.write(paste.contents)
# add a newline unless we are writing
# in a file or in a pipe
if output.isatty():
output.write('\n')
def do_post(self, line):
"""
post [FILENAME]
Submit a new paste.
The filename can be '-' for reading standard input (pipe).
If 'bin' is passed, file will be uploaded to binary services.
"""
return self._post(line, binary=False)
def do_post_bin(self, line):
"""
post_bin [FILENAME]
Submit a new paste.
The filename can be '-' for reading standard input (pipe).
File will be uploaded to binary services.
"""
return self._post(line, binary=True)
def _post(self, filename, binary):
use_stdin = (not filename or filename == '-')
if use_stdin:
if binary:
if sys.version_info.major >= 3:
contents = self.stdin.buffer.read()
else:
contents = self.stdin.read()
else:
contents = self.acquire_input()
if not len(contents):
print('Empty paste, aborting.', file=self.stderr)
return 1
else:
try:
if binary:
m = open(filename, 'rb')
else:
m = codecs.open(filename, encoding=self.options.encoding or self.encoding)
with m as fp:
contents = fp.read()
except IOError as e:
print('Unable to open file "%s": %s' % (filename, e.strerror), file=self.stderr)
return 1
if binary:
contents = b64encode(contents).decode('ascii')
# get and sort the backends able to satisfy our requirements
params = self.get_params()
backends = {}
for backend in self.weboob.iter_backends():
score = backend.can_post(contents, **params)
if score:
backends.setdefault(score, []).append(backend)
# select a random backend from the best scores
if len(backends):
backend = choice(backends[max(list(backends.keys()))])
else:
print('No suitable backend found.', file=self.stderr)
return 1
p = backend.new_paste(_id=None)
p.public = params['public']
if self.options.title is not None:
p.title = self.options.title
else:
p.title = os.path.basename(filename)
p.contents = contents
backend.post_paste(p, max_age=params['max_age'])
print('Successfuly posted paste: %s' % p.page_url)
def get_params(self):
return {'public': self.options.public,
'max_age': self.str_to_duration(self.options.max_age),
'title': self.options.title}
def str_to_duration(self, s):
if s.strip().lower() == 'never':
return False
parts = re.findall(r'(\d*(?:\.\d+)?)\s*([A-z]+)', s)
argsmap = {'Y|y|year|years|yr|yrs': 365.25 * 24 * 3600,
'M|o|month|months': 30.5 * 24 * 3600,
'W|w|week|weeks': 7 * 24 * 3600,
'D|d|day|days': 24 * 3600,
'H|h|hours|hour|hr|hrs': 3600,
'm|i|minute|minutes|min|mins': 60,
'S|s|second|seconds|sec|secs': 1}
seconds = 0
for number, unit in parts:
for rx, secs in argsmap.items():
if re.match('^(%s)$' % rx, unit):
seconds += float(number) * float(secs)
return int(seconds)
def add_application_options(self, group):
group.add_option('-p', '--public', action='store_true',
help='Make paste public.')
group.add_option('-t', '--title', action='store',
help='Paste title',
type='string')
group.add_option('-m', '--max-age', action='store',
help='Maximum age (duration), default "1 month", "never" for infinite',
type='string', default='1 month')
group.add_option('-E', '--encoding', action='store',
help='Input encoding',
type='string')
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ 0000775 0000000 0000000 00000000000 13204006733 0026613 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/__init__.py 0000664 0000000 0000000 00000000100 13204006733 0030713 0 ustar 00root root 0000000 0000000 from .qbooblyrics import QBooblyrics
__all__ = ['QBooblyrics']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/main_window.py 0000664 0000000 0000000 00000027011 13204006733 0031501 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from PyQt5.QtCore import pyqtSlot as Slot, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QFrame, QShortcut
from weboob.capabilities.lyrics import CapLyrics
from weboob.tools.application.qt5 import QtMainWindow, QtDo
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.models import BackendListModel
from weboob.tools.application.qt5.search_history import HistoryCompleter
from weboob.applications.qbooblyrics.ui.main_window_ui import Ui_MainWindow
from weboob.applications.qbooblyrics.ui.result_ui import Ui_Result
from .minisonglyrics import MiniSonglyrics
from .songlyrics import Songlyrics
MAX_TAB_TEXT_LENGTH=30
class Result(QFrame):
def __init__(self, weboob, app, parent=None):
super(Result, self).__init__(parent)
self.ui = Ui_Result()
self.ui.setupUi(self)
self.parent = parent
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# action history is composed by the last action and the action list
# An action is a function, a list of arguments and a description string
self.action_history = {'last_action': None, 'action_list': []}
self.ui.backButton.clicked.connect(self.doBack)
self.ui.backButton.setShortcut(QKeySequence('Alt+Left'))
self.ui.backButton.hide()
def doAction(self, description, fun, args):
''' Call fun with args as arguments
and save it in the action history
'''
self.ui.currentActionLabel.setText(description)
if self.action_history['last_action'] is not None:
self.action_history['action_list'].append(self.action_history['last_action'])
self.ui.backButton.setToolTip('%s (Alt+Left)'%self.action_history['last_action']['description'])
self.ui.backButton.show()
self.action_history['last_action'] = {'function': fun, 'args': args, 'description': description}
# manage tab text
mytabindex = self.parent.ui.resultsTab.indexOf(self)
tabtxt = description
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
self.parent.ui.resultsTab.setTabText(mytabindex, tabtxt)
self.parent.ui.resultsTab.setTabToolTip(mytabindex, description)
return fun(*args)
@Slot()
def doBack(self):
''' Go back in action history
Basically call previous function and update history
'''
if len(self.action_history['action_list']) > 0:
todo = self.action_history['action_list'].pop()
self.ui.currentActionLabel.setText(todo['description'])
self.action_history['last_action'] = todo
if len(self.action_history['action_list']) == 0:
self.ui.backButton.hide()
else:
self.ui.backButton.setToolTip(self.action_history['action_list'][-1]['description'])
# manage tab text
mytabindex = self.parent.ui.resultsTab.indexOf(self)
tabtxt = todo['description']
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
self.parent.ui.resultsTab.setTabText(mytabindex, tabtxt)
self.parent.ui.resultsTab.setTabToolTip(mytabindex, todo['description'])
return todo['function'](*todo['args'])
def processFinished(self):
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
@Slot()
def stopProcess(self):
if self.process is not None:
self.process.stop()
def searchSonglyrics(self,pattern):
if not pattern:
return
self.doAction(u'Search lyrics "%s"' % pattern, self.searchSonglyricsAction, [pattern])
def searchSonglyricsAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex())
self.process = QtDo(self.weboob, self.addSonglyrics, fb=self.processFinished)
self.process.do(self.app._do_complete, self.parent.getCount(), ('title'), 'iter_lyrics',
self.parent.ui.typeCombo.currentText(), pattern, backends=backend_name, caps=CapLyrics)
self.parent.ui.stopButton.show()
def addSonglyrics(self, songlyrics):
minisonglyrics = MiniSonglyrics(self.weboob, self.weboob[songlyrics.backend], songlyrics, self)
self.ui.list_content.layout().insertWidget(self.ui.list_content.layout().count()-1,minisonglyrics)
self.minis.append(minisonglyrics)
def displaySonglyrics(self, songlyrics, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wsonglyrics = Songlyrics(songlyrics, backend, self)
self.ui.info_content.layout().addWidget(wsonglyrics)
self.current_info_widget = wsonglyrics
QApplication.restoreOverrideCursor()
def searchId(self, id):
QApplication.setOverrideCursor(Qt.WaitCursor)
if '@' in id:
backend_name = id.split('@')[1]
id = id.split('@')[0]
else:
backend_name = None
for backend in self.weboob.iter_backends():
if (backend_name and backend.name == backend_name) or not backend_name:
songlyrics = backend.get_lyrics(id)
if songlyrics:
self.doAction('Lyrics of "%s" (%s)' % (songlyrics.title, songlyrics.artist), self.displaySonglyrics, [songlyrics, backend])
QApplication.restoreOverrideCursor()
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# search history is a list of patterns which have been searched
history_path = os.path.join(self.weboob.workdir, 'qbooblyrics_history')
qc = HistoryCompleter(history_path, self)
qc.load()
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
self.ui.typeCombo.addItem('song')
self.ui.typeCombo.addItem('artist')
self.ui.searchEdit.returnPressed.connect(self.search)
self.ui.idEdit.returnPressed.connect(self.searchId)
count = self.config.get('settings', 'maxresultsnumber')
self.ui.countSpin.setValue(int(count))
showT = self.config.get('settings', 'showthumbnails')
self.ui.showTCheck.setChecked(showT == '1')
self.ui.stopButton.hide()
self.ui.actionBackends.triggered.connect(self.backendsConfig)
q = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self)
q.activated.connect(self.close)
n = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageDown), self)
n.activated.connect(self.nextTab)
p = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageUp), self)
p.activated.connect(self.prevTab)
w = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_W), self)
w.activated.connect(self.closeCurrentTab)
l = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_L), self)
l.activated.connect(self.ui.searchEdit.setFocus)
l.activated.connect(self.ui.searchEdit.selectAll)
self.ui.resultsTab.tabCloseRequested.connect(self.closeTab)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapLyrics, ), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
model = BackendListModel(self.weboob)
model.addBackends()
self.ui.backendEdit.setModel(model)
current_backend = self.config.get('settings', 'backend')
idx = self.ui.backendEdit.findData(current_backend)
if idx >= 0:
self.ui.backendEdit.setCurrentIndex(idx)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
def getCount(self):
num = self.ui.countSpin.value()
if num == 0:
return None
else:
return num
@Slot(int)
def closeTab(self, index):
if self.ui.resultsTab.widget(index) != 0:
self.ui.resultsTab.removeTab(index)
@Slot()
def closeCurrentTab(self):
self.closeTab(self.ui.resultsTab.currentIndex())
@Slot()
def prevTab(self):
index = self.ui.resultsTab.currentIndex() - 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
@Slot()
def nextTab(self):
index = self.ui.resultsTab.currentIndex() + 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
def newTab(self, txt, backend, songlyrics=None):
id = ''
if songlyrics is not None:
id = songlyrics.id
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, txt)
new_res.searchId('%s@%s'%(id,backend.NAME))
self.ui.stopButton.clicked.connect(new_res.stopProcess)
@Slot()
def search(self):
pattern = self.ui.searchEdit.text()
self.ui.searchEdit.completer().addString(pattern)
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, pattern)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.searchSonglyrics(pattern)
@Slot()
def searchId(self):
id = self.ui.idEdit.text()
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, id)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.searchId(id)
def closeEvent(self, ev):
self.config.set('settings', 'backend', self.ui.backendEdit.itemData(
self.ui.backendEdit.currentIndex()))
self.ui.searchEdit.completer().save()
self.config.set('settings', 'maxresultsnumber', self.ui.countSpin.value())
self.config.save()
ev.accept()
minisonglyrics.py 0000664 0000000 0000000 00000006342 13204006733 0032164 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics # -*- coding: utf-8 -*-
# Copyright(C) 2016 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtGui import QIcon, QImage, QPixmap, QPixmapCache
from PyQt5.QtWidgets import QFrame, QApplication
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.applications.qbooblyrics.ui.minisonglyrics_ui import Ui_MiniSonglyrics
from weboob.capabilities.base import empty
class MiniSonglyrics(QFrame):
def __init__(self, weboob, backend, songlyrics, parent=None):
super(MiniSonglyrics, self).__init__(parent)
self.parent = parent
self.ui = Ui_MiniSonglyrics()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.songlyrics = songlyrics
self.ui.titleLabel.setText(songlyrics.title)
if not empty(songlyrics.artist):
if len(songlyrics.artist) > 300:
self.ui.artistLabel.setText('%s [...]'%songlyrics.artist[:300])
else:
self.ui.artistLabel.setText(songlyrics.artist)
else:
self.ui.artistLabel.setText('')
self.ui.backendButton.setText(backend.name)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
if icon_path:
pixmap = QPixmapCache.find(icon_path)
if not pixmap:
pixmap = QPixmap(QImage(icon_path))
self.ui.backendButton.setIcon(QIcon(pixmap))
self.ui.newTabButton.clicked.connect(self.newTabPressed)
self.ui.viewButton.clicked.connect(self.viewPressed)
@Slot()
def viewPressed(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
songlyrics = self.backend.get_lyrics(self.songlyrics.id)
if songlyrics:
self.parent.doAction('Lyrics of "%s" (%s)' %
(songlyrics.title, songlyrics.artist), self.parent.displaySonglyrics, [songlyrics, self.backend])
@Slot()
def newTabPressed(self):
songlyrics = self.backend.get_lyrics(self.songlyrics.id)
self.parent.parent.newTab(u'Lyrics of "%s" (%s)' %
(songlyrics.title, songlyrics.artist), self.backend, songlyrics=songlyrics)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 4:
self.newTabPressed()
else:
self.viewPressed()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/qbooblyrics.py 0000664 0000000 0000000 00000002732 13204006733 0031521 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.lyrics import CapLyrics
from weboob.tools.application.qt5 import QtApplication
from .main_window import MainWindow
class QBooblyrics(QtApplication):
APPNAME = 'qbooblyrics'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2016 Julien Veyssier'
DESCRIPTION = "Qt application allowing to search song lyrics."
SHORT_DESCRIPTION = "search lyrics"
CAPS = CapLyrics
CONFIG = {'settings': {'backend': '',
'maxresultsnumber': '10'
}
}
def main(self, argv):
self.load_backends([CapLyrics])
self.load_config()
main_window = MainWindow(self.config, self.weboob, self)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/songlyrics.py 0000664 0000000 0000000 00000003516 13204006733 0031366 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QFrame
from weboob.applications.qbooblyrics.ui.songlyrics_ui import Ui_Songlyrics
from weboob.capabilities.base import empty
class Songlyrics(QFrame):
def __init__(self, songlyrics, backend, parent=None):
super(Songlyrics, self).__init__(parent)
self.parent = parent
self.ui = Ui_Songlyrics()
self.ui.setupUi(self)
self.songlyrics = songlyrics
self.backend = backend
self.ui.idEdit.setText(u'%s@%s' % (songlyrics.id, backend.name))
if not empty(songlyrics.title):
self.ui.titleLabel.setText(songlyrics.title)
if not empty(songlyrics.artist):
self.ui.artistLabel.setText(songlyrics.artist)
else:
self.ui.artistLabel.parent().hide()
if not empty(songlyrics.content):
self.ui.lyricsPlain.setPlainText(songlyrics.content)
else:
self.ui.lyricsPlain.parent().hide()
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui/ 0000775 0000000 0000000 00000000000 13204006733 0027230 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030673 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0031327 0 ustar 00root root 0000000 0000000 main_window.ui 0000664 0000000 0000000 00000010572 13204006733 0032030 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui
MainWindow00748463QBooblyrics4QFrame::StyledPanelQFrame::Raised00Search: 3516777215background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.479904 rgba(255, 0, 0, 255), stop:0.522685 rgba(255, 255, 255, 255), stop:1 rgba(255, 255, 255, 255));stop<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>max results<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>-1truetrueShow thumbnailssearch by ID:0074825toolBarTopToolBarAreafalseBackends
minisonglyrics.ui 0000664 0000000 0000000 00000012155 13204006733 0032565 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui
MiniSonglyrics00578136FormQFrame::StyledPanelQFrame::Raised95500TextLabeltrue0050truefalseTextLabeltrue75trueArtist75trueWhere75trueTitle12020View12020View in new tabQt::Horizontal40201677721520PushButtontrueQt::Horizontal4020
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui/result.ui 0000664 0000000 0000000 00000011172 13204006733 0031107 0 ustar 00root root 0000000 0000000
Result00645733001000010000FrameQFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::Raised00Qt::Horizontal40206020<<back75trueQt::AlignCentertrueQt::Horizontal4020true00605667Qt::Vertical2040true00605667
songlyrics.ui 0000664 0000000 0000000 00000015521 13204006733 0031710 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qbooblyrics/ui
Songlyrics006458890080005000FrameQFrame::StyledPanelQFrame::Raised167772153000QFrame::NoFrameQFrame::Raised167772154000QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueTitle:1677721535QFrame::StyledPanelQFrame::Raised75trueArtist:00167772151000QFrame::StyledPanelQFrame::Raised75trueLyrics0100Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/ 0000775 0000000 0000000 00000000000 13204006733 0026074 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/__init__.py 0000664 0000000 0000000 00000000067 13204006733 0030210 0 ustar 00root root 0000000 0000000 from .qboobmsg import QBoobMsg
__all__ = ['QBoobMsg']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/main_window.py 0000664 0000000 0000000 00000003602 13204006733 0030762 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtCore import pyqtSlot as Slot
from weboob.tools.application.qt5 import QtMainWindow
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.capabilities.messages import CapMessages
from .ui.main_window_ui import Ui_MainWindow
from .messages_manager import MessagesManager
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.manager = MessagesManager(weboob, self)
self.setCentralWidget(self.manager)
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.ui.actionRefresh.triggered.connect(self.refresh)
if self.weboob.count_backends() == 0:
self.backendsConfig()
else:
self.centralWidget().load()
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapMessages,), self)
if bckndcfg.run():
self.centralWidget().load()
@Slot()
def refresh(self):
self.centralWidget().refreshThreads()
messages_manager.py 0000664 0000000 0000000 00000024530 13204006733 0031674 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import time
import logging
from PyQt5.QtWidgets import QWidget, QTreeWidgetItem, QListWidgetItem, QMessageBox
from PyQt5.QtGui import QBrush
from PyQt5.QtCore import Qt, pyqtSignal as Signal, pyqtSlot as Slot
from weboob.capabilities.messages import CapMessages, CapMessagesPost, Message
from weboob.tools.application.qt5 import QtDo
from weboob.tools.misc import to_unicode
from .ui.messages_manager_ui import Ui_MessagesManager
class MessagesManager(QWidget):
display_contact = Signal(object)
def __init__(self, weboob, parent=None):
super(MessagesManager, self).__init__(parent)
self.ui = Ui_MessagesManager()
self.ui.setupUi(self)
self.weboob = weboob
self.ui.backendsList.setCurrentRow(0)
self.backend = None
self.thread = None
self.message = None
self.ui.replyButton.setEnabled(False)
self.ui.replyWidget.hide()
self.ui.backendsList.itemSelectionChanged.connect(self._backendChanged)
self.ui.threadsList.itemSelectionChanged.connect(self._threadChanged)
self.ui.messagesTree.itemClicked.connect(self._messageSelected)
self.ui.messagesTree.itemActivated.connect(self._messageSelected)
self.ui.profileButton.clicked.connect(self._profilePressed)
self.ui.replyButton.clicked.connect(self._replyPressed)
self.ui.sendButton.clicked.connect(self._sendPressed)
def load(self):
self.ui.backendsList.clear()
self.ui.backendsList.addItem('(All)')
for backend in self.weboob.iter_backends():
if not backend.has_caps(CapMessages):
continue
item = QListWidgetItem(backend.name.capitalize())
item.setData(Qt.UserRole, backend)
self.ui.backendsList.addItem(item)
self.refreshThreads()
@Slot()
def _backendChanged(self):
selection = self.ui.backendsList.selectedItems()
if not selection:
self.backend = None
return
self.backend = selection[0].data(Qt.UserRole)
self.refreshThreads()
def refreshThreads(self):
self.ui.messagesTree.clear()
self.ui.threadsList.clear()
self.hideReply()
self.ui.profileButton.hide()
self.ui.replyButton.setEnabled(False)
self.ui.backendsList.setEnabled(False)
self.ui.threadsList.setEnabled(False)
self.process_threads = QtDo(self.weboob, self._gotThread, fb=self._gotThreadsEnd)
self.process_threads.do('iter_threads', backends=self.backend, caps=CapMessages)
def _gotThreadsEnd(self):
self.process_threads = None
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
def _gotThread(self, thread):
item = QListWidgetItem(thread.title)
item.setData(Qt.UserRole, (thread.backend, thread.id))
self.ui.threadsList.addItem(item)
@Slot()
def _threadChanged(self):
self.ui.messagesTree.clear()
selection = self.ui.threadsList.selectedItems()
if not selection:
return
t = selection[0].data(Qt.UserRole)
self.refreshThreadMessages(*t)
def refreshThreadMessages(self, backend, id):
self.ui.messagesTree.clear()
self.ui.messageBody.clear()
self.ui.backendsList.setEnabled(False)
self.ui.threadsList.setEnabled(False)
self.ui.replyButton.setEnabled(False)
self.ui.profileButton.hide()
self.hideReply()
self.process = QtDo(self.weboob, self._gotThreadMessages, fb=self._gotThreadMessagesEnd)
self.process.do('get_thread', id, backends=backend)
def _gotThreadMessagesEnd(self):
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
self.process = None
def _gotThreadMessages(self, thread):
self.thread = thread
if thread.flags & thread.IS_THREADS:
top = self.ui.messagesTree.invisibleRootItem()
else:
top = None
self._insert_message(thread.root, top)
self.showMessage(thread.root)
self.ui.messagesTree.expandAll()
def _insert_message(self, message, top):
item = QTreeWidgetItem(None, [message.title or '', message.sender or 'Unknown',
time.strftime('%Y-%m-%d %H:%M:%S', message.date.timetuple())])
item.setData(0, Qt.UserRole, message)
if message.flags & message.IS_UNREAD:
item.setForeground(0, QBrush(Qt.darkYellow))
item.setForeground(1, QBrush(Qt.darkYellow))
item.setForeground(2, QBrush(Qt.darkYellow))
if top is not None:
# threads
top.addChild(item)
else:
# discussion
self.ui.messagesTree.invisibleRootItem().insertChild(0, item)
if message.children is not None:
for child in message.children:
self._insert_message(child, top and item)
@Slot(QTreeWidgetItem, int)
def _messageSelected(self, item, column):
message = item.data(0, Qt.UserRole)
self.showMessage(message, item)
def showMessage(self, message, item=None):
backend = self.weboob.get_backend(message.thread.backend)
if backend.has_caps(CapMessagesPost):
self.ui.replyButton.setEnabled(True)
self.message = message
if message.title.startswith('Re:'):
self.ui.titleEdit.setText(message.title)
else:
self.ui.titleEdit.setText('Re: %s' % message.title)
if message.flags & message.IS_HTML:
content = message.content
else:
content = message.content.replace('&', '&').replace('<', '<').replace('>', '>').replace('\n', ' ')
extra = u''
if message.flags & message.IS_NOT_RECEIVED:
extra += u'Status: Unread '
elif message.flags & message.IS_RECEIVED:
extra += u'Status: Read '
elif message.flags & message.IS_UNREAD:
extra += u'Status: New '
self.ui.messageBody.setText("
%s
"
"Date: %s "
"From: %s "
"%s"
"
%s
"
% (message.title, str(message.date), message.sender, extra, content))
if item and message.flags & message.IS_UNREAD:
backend.set_message_read(message)
message.flags &= ~message.IS_UNREAD
item.setForeground(0, QBrush())
item.setForeground(1, QBrush())
item.setForeground(2, QBrush())
if message.thread.flags & message.thread.IS_DISCUSSION:
self.ui.profileButton.show()
else:
self.ui.profileButton.hide()
@Slot()
def _profilePressed(self):
print(self.thread.id)
self.display_contact.emit(self.thread.id)
def displayReply(self):
self.ui.replyButton.setText(self.tr('Cancel'))
self.ui.replyWidget.show()
def hideReply(self):
self.ui.replyButton.setText(self.tr('Reply'))
self.ui.replyWidget.hide()
self.ui.replyEdit.clear()
self.ui.titleEdit.clear()
@Slot()
def _replyPressed(self):
if self.ui.replyWidget.isVisible():
self.hideReply()
else:
self.displayReply()
@Slot()
def _sendPressed(self):
if not self.ui.replyWidget.isVisible():
return
text = self.ui.replyEdit.toPlainText()
title = self.ui.titleEdit.text()
self.ui.backendsList.setEnabled(False)
self.ui.threadsList.setEnabled(False)
self.ui.messagesTree.setEnabled(False)
self.ui.replyButton.setEnabled(False)
self.ui.replyWidget.setEnabled(False)
self.ui.sendButton.setText(self.tr('Sending...'))
flags = 0
if self.ui.htmlBox.currentIndex() == 0:
flags = Message.IS_HTML
m = Message(thread=self.thread,
id=0,
title=title,
sender=None,
receivers=None,
content=text,
parent=self.message,
flags=flags)
self.process_reply = QtDo(self.weboob, None, self._postReply_eb, self._postReply_fb)
self.process_reply.do('post_message', m, backends=self.thread.backend)
def _postReply_fb(self):
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
self.ui.messagesTree.setEnabled(True)
self.ui.replyButton.setEnabled(True)
self.ui.replyWidget.setEnabled(True)
self.ui.sendButton.setEnabled(True)
self.ui.sendButton.setText(self.tr('Send'))
self.hideReply()
self.process_reply = None
self.refreshThreadMessages(self.thread.backend, self.thread.id)
def _postReply_eb(self, backend, error, backtrace):
content = self.tr('Unable to send message:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while posting reply'),
content, QMessageBox.Ok)
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
self.ui.messagesTree.setEnabled(True)
self.ui.replyButton.setEnabled(True)
self.ui.replyWidget.setEnabled(True)
self.ui.sendButton.setText(self.tr('Send'))
self.process_reply = None
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/qboobmsg.py 0000664 0000000 0000000 00000002607 13204006733 0030264 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.messages import CapMessages
from weboob.tools.application.qt5 import QtApplication
from .main_window import MainWindow
class QBoobMsg(QtApplication):
APPNAME = 'qboobmsg'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Qt application allowing to read messages on various websites and reply to them."
SHORT_DESCRIPTION = "send and receive message threads"
CAPS = CapMessages
def main(self, argv):
self.load_backends(CapMessages, storage=self.create_storage())
main_window = MainWindow(self.config, self.weboob)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026511 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030154 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030610 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/ui/main_window.ui 0000664 0000000 0000000 00000004307 13204006733 0031367 0 ustar 00root root 0000000 0000000
MainWindow00763580QBoobMsg0076320FiletoolBarTopToolBarAreafalseBackendsQuitQuitCtrl+QRefreshactionQuittriggered()MainWindowclose()-1-1381289
messages_manager.ui 0000664 0000000 0000000 00000021205 13204006733 0032272 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qboobmsg/ui
MessagesManager00696591Qt::Horizontal0015016777215Qt::HorizontalQt::Vertical0050QFrame::StyledPanelQFrame::Raised929200+00−Qt::Horizontal4020QAbstractItemView::NoEditTriggerstruetruetruetrue150trueTitleFromDateQt::Vertical05trueQt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouseQFrame::StyledPanelQFrame::RaisedProfileReply0With HTMLWithout HTMLSendexpandButtonclicked()messagesTreeexpandAll()73331527150collapseButtonclicked()messagesTreecollapseAll()73360527150
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ 0000775 0000000 0000000 00000000000 13204006733 0026062 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/__init__.py 0000664 0000000 0000000 00000000067 13204006733 0030176 0 ustar 00root root 0000000 0000000 from .qcineoob import QCineoob
__all__ = ['QCineoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/main_window.py 0000664 0000000 0000000 00000060300 13204006733 0030746 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QFrame, QShortcut
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.cinema import CapCinema
from weboob.capabilities.torrent import CapTorrent
from weboob.capabilities.subtitle import CapSubtitle
from weboob.tools.application.qt5 import QtMainWindow, QtDo
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.models import BackendListModel
from weboob.tools.application.qt5.search_history import HistoryCompleter
from weboob.tools.compat import unicode
from weboob.applications.suboob.suboob import LANGUAGE_CONV
from weboob.applications.qcineoob.ui.main_window_ui import Ui_MainWindow
from weboob.applications.qcineoob.ui.result_ui import Ui_Result
from .minimovie import MiniMovie
from .miniperson import MiniPerson
from .minitorrent import MiniTorrent
from .minisubtitle import MiniSubtitle
from .movie import Movie
from .person import Person
from .torrent import Torrent
from .subtitle import Subtitle
MAX_TAB_TEXT_LENGTH=30
class Result(QFrame):
def __init__(self, weboob, app, parent=None):
super(Result, self).__init__(parent)
self.ui = Ui_Result()
self.ui.setupUi(self)
self.parent = parent
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# action history is composed by the last action and the action list
# An action is a function, a list of arguments and a description string
self.action_history = {'last_action': None, 'action_list': []}
self.ui.backButton.clicked.connect(self.doBack)
self.ui.backButton.setShortcut(QKeySequence('Alt+Left'))
self.ui.backButton.hide()
def doAction(self, description, fun, args):
''' Call fun with args as arguments
and save it in the action history
'''
self.ui.currentActionLabel.setText(description)
if self.action_history['last_action'] is not None:
self.action_history['action_list'].append(self.action_history['last_action'])
self.ui.backButton.setToolTip('%s (Alt+Left)'%self.action_history['last_action']['description'])
self.ui.backButton.show()
self.action_history['last_action'] = {'function': fun, 'args': args, 'description': description}
# manage tab text
mytabindex = self.parent.ui.resultsTab.indexOf(self)
tabtxt = description
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
self.parent.ui.resultsTab.setTabText(mytabindex, tabtxt)
self.parent.ui.resultsTab.setTabToolTip(mytabindex, description)
return fun(*args)
@Slot()
def doBack(self):
''' Go back in action history
Basically call previous function and update history
'''
if len(self.action_history['action_list']) > 0:
todo = self.action_history['action_list'].pop()
self.ui.currentActionLabel.setText(todo['description'])
self.action_history['last_action'] = todo
if len(self.action_history['action_list']) == 0:
self.ui.backButton.hide()
else:
self.ui.backButton.setToolTip(self.action_history['action_list'][-1]['description'])
# manage tab text
mytabindex = self.parent.ui.resultsTab.indexOf(self)
tabtxt = todo['description']
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
self.parent.ui.resultsTab.setTabText(mytabindex, tabtxt)
self.parent.ui.resultsTab.setTabToolTip(mytabindex, todo['description'])
return todo['function'](*todo['args'])
def castingAction(self, backend_name, id, role):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
self.process = QtDo(self.weboob, self.addPerson, fb=self.processFinished)
self.process.do('iter_movie_persons', id, role, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def moviesInCommonAction(self, backend_name, id1, id2):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
for a_backend in self.weboob.iter_backends():
if (backend_name and a_backend.name == backend_name):
backend = a_backend
person1 = backend.get_person(id1)
person2 = backend.get_person(id2)
lid1 = []
for p in backend.iter_person_movies_ids(id1):
lid1.append(p)
lid2 = []
for p in backend.iter_person_movies_ids(id2):
lid2.append(p)
inter = list(set(lid1) & set(lid2))
chrono_list = []
for common in inter:
movie = backend.get_movie(common)
movie.backend = backend_name
role1 = movie.get_roles_by_person_id(person1.id)
role2 = movie.get_roles_by_person_id(person2.id)
if (movie.release_date != NotAvailable):
year = movie.release_date.year
else:
year = '????'
movie.short_description = '(%s) %s as %s ; %s as %s'%(year , person1.name, ', '.join(role1), person2.name, ', '.join(role2))
i = 0
while (i chrono_list[i].release_date.year)):
i += 1
chrono_list.insert(i, movie)
for movie in chrono_list:
self.addMovie(movie)
self.processFinished()
def personsInCommonAction(self, backend_name, id1, id2):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
for a_backend in self.weboob.iter_backends():
if (backend_name and a_backend.name == backend_name):
backend = a_backend
movie1 = backend.get_movie(id1)
movie2 = backend.get_movie(id2)
lid1 = []
for p in backend.iter_movie_persons_ids(id1):
lid1.append(p)
lid2 = []
for p in backend.iter_movie_persons_ids(id2):
lid2.append(p)
inter = list(set(lid1) & set(lid2))
for common in inter:
person = backend.get_person(common)
person.backend = backend_name
role1 = movie1.get_roles_by_person_id(person.id)
role2 = movie2.get_roles_by_person_id(person.id)
person.short_description = '%s in %s ; %s in %s'%(', '.join(role1), movie1.original_title, ', '.join(role2), movie2.original_title)
self.addPerson(person)
self.processFinished()
def filmographyAction(self, backend_name, id, role):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
self.process = QtDo(self.weboob, self.addMovie, fb=self.processFinished)
self.process.do('iter_person_movies', id, role, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def search(self, tosearch, pattern, lang):
if tosearch == 'person':
self.searchPerson(pattern)
elif tosearch == 'movie':
self.searchMovie(pattern)
elif tosearch == 'torrent':
self.searchTorrent(pattern)
elif tosearch == 'subtitle':
self.searchSubtitle(lang, pattern)
def searchMovie(self, pattern):
if not pattern:
return
self.doAction(u'Search movie "%s"' % pattern, self.searchMovieAction, [pattern])
def searchMovieAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex())
self.process = QtDo(self.weboob, self.addMovie, fb=self.processFinished)
#self.process.do('iter_movies', pattern, backends=backend_name, caps=CapCinema)
self.process.do(self.app._do_complete, self.parent.getCount(), ('original_title'), 'iter_movies', pattern, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def addMovie(self, movie):
minimovie = MiniMovie(self.weboob, self.weboob[movie.backend], movie, self)
self.ui.list_content.layout().insertWidget(self.ui.list_content.layout().count()-1,minimovie)
self.minis.append(minimovie)
def displayMovie(self, movie, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wmovie = Movie(movie, backend, self)
self.ui.info_content.layout().addWidget(wmovie)
self.current_info_widget = wmovie
QApplication.restoreOverrideCursor()
def searchPerson(self, pattern):
if not pattern:
return
self.doAction(u'Search person "%s"' % pattern, self.searchPersonAction, [pattern])
def searchPersonAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex())
self.process = QtDo(self.weboob, self.addPerson, fb=self.processFinished)
#self.process.do('iter_persons', pattern, backends=backend_name, caps=CapCinema)
self.process.do(self.app._do_complete, self.parent.getCount(), ('name'), 'iter_persons', pattern, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def addPerson(self, person):
miniperson = MiniPerson(self.weboob, self.weboob[person.backend], person, self)
self.ui.list_content.layout().insertWidget(self.ui.list_content.layout().count()-1,miniperson)
self.minis.append(miniperson)
def displayPerson(self, person, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wperson = Person(person, backend, self)
self.ui.info_content.layout().addWidget(wperson)
self.current_info_widget = wperson
QApplication.restoreOverrideCursor()
def searchTorrent(self, pattern):
if not pattern:
return
self.doAction(u'Search torrent "%s"' % pattern, self.searchTorrentAction, [pattern])
def searchTorrentAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex())
self.process = QtDo(self.weboob, self.addTorrent, fb=self.processFinished)
#self.process.do('iter_torrents', pattern, backends=backend_name, caps=CapTorrent)
self.process.do(self.app._do_complete, self.parent.getCount(), ('name'), 'iter_torrents', pattern, backends=backend_name, caps=CapTorrent)
self.parent.ui.stopButton.show()
def processFinished(self):
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
@Slot()
def stopProcess(self):
if self.process is not None:
self.process.stop()
def addTorrent(self, torrent):
minitorrent = MiniTorrent(self.weboob, self.weboob[torrent.backend], torrent, self)
positionToInsert = self.ui.list_content.layout().count()-1
# if possible, we insert the torrent keeping a sort by seed
if torrent.seeders != NotAvailable:
seeders = torrent.seeders
positionToInsert = 0
while positionToInsert < len(self.minis) and\
(self.minis[positionToInsert].torrent.seeders != NotAvailable and\
seeders <= self.minis[positionToInsert].torrent.seeders):
positionToInsert += 1
self.ui.list_content.layout().insertWidget(positionToInsert, minitorrent)
self.minis.insert(positionToInsert, minitorrent)
def displayTorrent(self, torrent, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wtorrent = Torrent(torrent, backend, self)
self.ui.info_content.layout().addWidget(wtorrent)
self.current_info_widget = wtorrent
def searchSubtitle(self, lang, pattern):
if not pattern:
return
self.doAction(u'Search subtitle "%s" (lang:%s)' % (pattern, lang), self.searchSubtitleAction, [lang, pattern])
def searchSubtitleAction(self, lang, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex())
self.process = QtDo(self.weboob, self.addSubtitle, fb=self.processFinished)
#self.process.do('iter_subtitles', lang, pattern, backends=backend_name, caps=CapSubtitle)
self.process.do(self.app._do_complete, self.parent.getCount(), ('name'), 'iter_subtitles', lang, pattern, backends=backend_name, caps=CapSubtitle)
self.parent.ui.stopButton.show()
def addSubtitle(self, subtitle):
minisubtitle = MiniSubtitle(self.weboob, self.weboob[subtitle.backend], subtitle, self)
self.ui.list_content.layout().insertWidget(self.ui.list_content.layout().count()-1,minisubtitle)
self.minis.append(minisubtitle)
def displaySubtitle(self, subtitle, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wsubtitle = Subtitle(subtitle, backend, self)
self.ui.info_content.layout().addWidget(wsubtitle)
self.current_info_widget = wsubtitle
def searchId(self, id, stype):
QApplication.setOverrideCursor(Qt.WaitCursor)
title_field = 'name'
if stype == 'movie':
cap = CapCinema
title_field = 'original_title'
elif stype == 'person':
cap = CapCinema
elif stype == 'torrent':
cap = CapTorrent
elif stype == 'subtitle':
cap = CapSubtitle
if '@' in id:
backend_name = id.split('@')[1]
id = id.split('@')[0]
else:
backend_name = None
for backend in self.weboob.iter_backends():
if backend.has_caps(cap) and ((backend_name and backend.name == backend_name) or not backend_name):
exec('object = backend.get_%s(id)' % (stype))
if object:
func_display = 'self.display' + stype[0].upper() + stype[1:]
exec("self.doAction('Details of %s \"%%s\"' %% object.%s, %s, [object, backend])" %
(stype, title_field, func_display))
QApplication.restoreOverrideCursor()
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.app = app
# search history is a list of patterns which have been searched
history_path = os.path.join(self.weboob.workdir, 'qcineoob_history')
qc = HistoryCompleter(history_path, self)
qc.load()
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
self.ui.searchEdit.returnPressed.connect(self.search)
self.ui.idEdit.returnPressed.connect(self.searchId)
self.ui.typeCombo.currentIndexChanged[unicode].connect(self.typeComboChanged)
count = self.config.get('settings', 'maxresultsnumber')
self.ui.countSpin.setValue(int(count))
showT = self.config.get('settings', 'showthumbnails')
self.ui.showTCheck.setChecked(showT == '1')
self.ui.stopButton.hide()
self.ui.actionBackends.triggered.connect(self.backendsConfig)
q = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self)
q.activated.connect(self.close)
n = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageDown), self)
n.activated.connect(self.nextTab)
p = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageUp), self)
p.activated.connect(self.prevTab)
w = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_W), self)
w.activated.connect(self.closeCurrentTab)
l = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_L), self)
l.activated.connect(self.ui.searchEdit.setFocus)
l.activated.connect(self.ui.searchEdit.selectAll)
self.ui.resultsTab.tabCloseRequested.connect(self.closeTab)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
langs = sorted(LANGUAGE_CONV.keys())
for lang in langs:
self.ui.langCombo.addItem(lang)
self.ui.langCombo.hide()
self.ui.langLabel.hide()
@Slot(int)
def closeTab(self, index):
if self.ui.resultsTab.widget(index) != 0:
self.ui.resultsTab.removeTab(index)
@Slot()
def closeCurrentTab(self):
self.closeTab(self.ui.resultsTab.currentIndex())
@Slot()
def prevTab(self):
index = self.ui.resultsTab.currentIndex() - 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
@Slot()
def nextTab(self):
index = self.ui.resultsTab.currentIndex() + 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
def newTab(self, txt, backend, person=None, movie=None, torrent=None, subtitle=None):
id = ''
if person is not None:
id = person.id
stype = 'person'
elif movie is not None:
id = movie.id
stype = 'movie'
elif subtitle is not None:
id = subtitle.id
stype = 'subtitle'
elif torrent is not None:
id = torrent.id
stype = 'torrent'
new_res = Result(self.weboob, self.app, self)
self.ui.stopButton.clicked.connect(new_res.stopProcess)
tabtxt = txt
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
index = self.ui.resultsTab.addTab(new_res, tabtxt)
self.ui.resultsTab.setTabToolTip(index, txt)
new_res.searchId(id, stype)
@Slot()
def search(self):
pattern = self.ui.searchEdit.text()
self.ui.searchEdit.completer().addString(pattern)
tosearch = self.ui.typeCombo.currentText()
lang = self.ui.langCombo.currentText()
new_res = Result(self.weboob, self.app, self)
txt = 'search %s "%s"'%(tosearch, pattern)
tabtxt = txt
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
index = self.ui.resultsTab.addTab(new_res, tabtxt)
self.ui.resultsTab.setTabToolTip(index, txt)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.search(tosearch, pattern, lang)
@Slot()
def searchId(self):
id = self.ui.idEdit.text()
stype = self.ui.idTypeCombo.currentText()
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, id)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.searchId(id, stype)
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapCinema, CapTorrent, CapSubtitle,), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
model = BackendListModel(self.weboob)
model.addBackends()
self.ui.backendEdit.setModel(model)
current_backend = self.config.get('settings', 'backend')
idx = self.ui.backendEdit.findData(current_backend)
if idx >= 0:
self.ui.backendEdit.setCurrentIndex(idx)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
@Slot(unicode)
def typeComboChanged(self, value):
if value == 'subtitle':
self.ui.langCombo.show()
self.ui.langLabel.show()
else:
self.ui.langCombo.hide()
self.ui.langLabel.hide()
def getCount(self):
num = self.ui.countSpin.value()
if num == 0:
return None
else:
return num
def closeEvent(self, ev):
self.config.set('settings', 'backend', self.ui.backendEdit.itemData(
self.ui.backendEdit.currentIndex()))
self.ui.searchEdit.completer().save()
self.config.set('settings', 'maxresultsnumber', self.ui.countSpin.value())
self.config.set('settings', 'showthumbnails', '1' if self.ui.showTCheck.isChecked() else '0')
self.config.save()
ev.accept()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/minimovie.py 0000664 0000000 0000000 00000007064 13204006733 0030437 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests
from PyQt5.QtGui import QIcon, QImage, QPixmap, QPixmapCache
from PyQt5.QtWidgets import QFrame, QApplication
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.applications.qcineoob.ui.minimovie_ui import Ui_MiniMovie
from weboob.capabilities.base import empty, NotAvailable
class MiniMovie(QFrame):
def __init__(self, weboob, backend, movie, parent=None):
super(MiniMovie, self).__init__(parent)
self.parent = parent
self.ui = Ui_MiniMovie()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.movie = movie
self.ui.titleLabel.setText(movie.original_title)
self.ui.shortDescLabel.setText(movie.short_description)
self.ui.backendButton.setText(backend.name)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
if icon_path:
pixmap = QPixmapCache.find(icon_path)
if not pixmap:
pixmap = QPixmap(QImage(icon_path))
self.ui.backendButton.setIcon(QIcon(pixmap))
self.ui.newTabButton.clicked.connect(self.newTabPressed)
self.ui.viewButton.clicked.connect(self.viewPressed)
self.ui.viewThumbnailButton.clicked.connect(self.gotThumbnail)
if self.parent.parent.ui.showTCheck.isChecked():
self.gotThumbnail()
@Slot()
def gotThumbnail(self):
if empty(self.movie.thumbnail_url) and self.movie.thumbnail_url != NotAvailable:
self.backend.fill_movie(self.movie, ('thumbnail_url'))
if not empty(self.movie.thumbnail_url):
data = requests.get(self.movie.thumbnail_url).content
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToHeight(100,Qt.SmoothTransformation))
@Slot()
def viewPressed(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
movie = self.backend.get_movie(self.movie.id)
if movie:
self.parent.doAction('Movie "%s"' %
movie.original_title, self.parent.displayMovie, [movie, self.backend])
@Slot()
def newTabPressed(self):
movie = self.backend.get_movie(self.movie.id)
self.parent.parent.newTab(u'Movie "%s"' %
movie.original_title, self.backend, movie=movie)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 2:
self.gotThumbnail()
elif event.button() == 4:
self.newTabPressed()
else:
self.viewPressed()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/miniperson.py 0000664 0000000 0000000 00000007552 13204006733 0030630 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests
from PyQt5.QtGui import QIcon, QImage, QPixmap, QPixmapCache
from PyQt5.QtWidgets import QFrame, QApplication
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.applications.qcineoob.ui.miniperson_ui import Ui_MiniPerson
from weboob.capabilities.base import empty, NotAvailable
class MiniPerson(QFrame):
def __init__(self, weboob, backend, person, parent=None):
super(MiniPerson, self).__init__(parent)
self.parent = parent
self.ui = Ui_MiniPerson()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.person = person
self.ui.nameLabel.setText('%s' % person.name)
if not empty(person.short_description):
if self.parent.ui.currentActionLabel.text().startswith('Casting'):
self.ui.shortDescTitleLabel.setText(u'Role')
self.ui.shortDescLabel.setText('%s' % person.short_description)
else:
self.ui.shortDescTitleLabel.hide()
self.ui.shortDescLabel.hide()
self.ui.backendButton.setText(backend.name)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
if icon_path:
pixmap = QPixmapCache.find(icon_path)
if not pixmap:
pixmap = QPixmap(QImage(icon_path))
self.ui.backendButton.setIcon(QIcon(pixmap))
self.ui.newTabButton.clicked.connect(self.newTabPressed)
self.ui.viewButton.clicked.connect(self.viewPressed)
self.ui.viewThumbnailButton.clicked.connect(self.gotThumbnail)
if self.parent.parent.ui.showTCheck.isChecked():
self.gotThumbnail()
@Slot()
def gotThumbnail(self):
if empty(self.person.thumbnail_url) and self.person.thumbnail_url != NotAvailable:
self.backend.fill_person(self.person, ('thumbnail_url'))
if not empty(self.person.thumbnail_url):
data = requests.get(self.person.thumbnail_url).content
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToHeight(100,Qt.SmoothTransformation))
@Slot()
def viewPressed(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
person = self.backend.get_person(self.person.id)
if person:
self.parent.doAction(u'Person "%s"' %
person.name, self.parent.displayPerson, [person, self.backend])
@Slot()
def newTabPressed(self):
person = self.backend.get_person(self.person.id)
self.parent.parent.newTab(u'Person "%s"' %
person.name, self.backend, person=person)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 2:
self.gotThumbnail()
elif event.button() == 4:
self.newTabPressed()
else:
self.viewPressed()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/minisubtitle.py 0000664 0000000 0000000 00000005553 13204006733 0031154 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QFrame
from PyQt5.QtCore import pyqtSlot as Slot
from PyQt5.QtGui import QIcon, QImage, QPixmap, QPixmapCache
from weboob.applications.qcineoob.ui.minisubtitle_ui import Ui_MiniSubtitle
from weboob.capabilities.base import empty
class MiniSubtitle(QFrame):
def __init__(self, weboob, backend, subtitle, parent=None):
super(MiniSubtitle, self).__init__(parent)
self.parent = parent
self.ui = Ui_MiniSubtitle()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.subtitle = subtitle
self.ui.nameLabel.setText(subtitle.name)
if not empty(subtitle.nb_cd):
self.ui.nbcdLabel.setText(u'%s' % subtitle.nb_cd)
self.ui.backendButton.setText(backend.name)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
if icon_path:
pixmap = QPixmapCache.find(icon_path)
if not pixmap:
pixmap = QPixmap(QImage(icon_path))
self.ui.backendButton.setIcon(QIcon(pixmap))
self.ui.newTabButton.clicked.connect(self.newTabPressed)
self.ui.viewButton.clicked.connect(self.viewPressed)
@Slot()
def viewPressed(self):
subtitle = self.backend.get_subtitle(self.subtitle.id)
if subtitle:
self.parent.doAction('Subtitle "%s"' %
subtitle.name, self.parent.displaySubtitle, [subtitle, self.backend])
@Slot()
def newTabPressed(self):
subtitle = self.backend.get_subtitle(self.subtitle.id)
self.parent.parent.newTab(u'Subtitle "%s"' %
subtitle.name, self.backend, subtitle=subtitle)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 4:
self.newTabPressed()
else:
self.viewPressed()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/minitorrent.py 0000664 0000000 0000000 00000006100 13204006733 0031003 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QFrame
from PyQt5.QtCore import pyqtSlot as Slot
from PyQt5.QtGui import QIcon, QImage, QPixmap, QPixmapCache
from weboob.applications.qcineoob.ui.minitorrent_ui import Ui_MiniTorrent
from weboob.applications.weboorrents.weboorrents import sizeof_fmt
from weboob.capabilities.base import empty
class MiniTorrent(QFrame):
def __init__(self, weboob, backend, torrent, parent=None):
super(MiniTorrent, self).__init__(parent)
self.parent = parent
self.ui = Ui_MiniTorrent()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.torrent = torrent
self.ui.nameLabel.setText(torrent.name)
if not empty(torrent.seeders) and not empty(torrent.leechers):
self.ui.seedLeechLabel.setText('%s/%s' % (torrent.seeders, torrent.leechers))
if not empty(torrent.size):
self.ui.sizeLabel.setText(u'%s' % sizeof_fmt(torrent.size))
self.ui.backendButton.setText(backend.name)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
if icon_path:
pixmap = QPixmapCache.find(icon_path)
if not pixmap:
pixmap = QPixmap(QImage(icon_path))
self.ui.backendButton.setIcon(QIcon(pixmap))
self.ui.newTabButton.clicked.connect(self.newTabPressed)
self.ui.viewButton.clicked.connect(self.viewPressed)
@Slot()
def viewPressed(self):
torrent = self.backend.get_torrent(self.torrent.id)
if torrent:
self.parent.doAction('Torrent "%s"' %
torrent.name, self.parent.displayTorrent, [torrent, self.backend])
@Slot()
def newTabPressed(self):
torrent = self.backend.get_torrent(self.torrent.id)
self.parent.parent.newTab(u'Torrent "%s"' %
torrent.name, self.backend, torrent=torrent)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 4:
self.newTabPressed()
else:
self.viewPressed()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/movie.py 0000664 0000000 0000000 00000013655 13204006733 0027565 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFrame, QMessageBox
from weboob.applications.qcineoob.ui.movie_ui import Ui_Movie
from weboob.capabilities.base import empty
from weboob.applications.suboob.suboob import LANGUAGE_CONV
from weboob.tools.compat import unicode
class Movie(QFrame):
def __init__(self, movie, backend, parent=None):
super(Movie, self).__init__(parent)
self.parent = parent
self.ui = Ui_Movie()
self.ui.setupUi(self)
langs = sorted(LANGUAGE_CONV.keys())
for lang in langs:
self.ui.langCombo.addItem(lang)
self.ui.castingButton.clicked.connect(self.casting)
self.ui.torrentButton.clicked.connect(self.searchTorrent)
self.ui.subtitleButton.clicked.connect(self.searchSubtitle)
self.ui.personsInCommonButton.clicked.connect(self.personsInCommon)
self.movie = movie
self.backend = backend
self.ui.titleLabel.setText(movie.original_title)
self.ui.durationLabel.setText(unicode(movie.duration))
self.gotThumbnail()
self.putReleases()
self.ui.idEdit.setText(u'%s@%s' % (movie.id, backend.name))
if not empty(movie.other_titles):
self.ui.otherTitlesPlain.setPlainText('\n'.join(movie.other_titles))
else:
self.ui.otherTitlesPlain.parent().hide()
if not empty(movie.genres):
genres = u''
for g in movie.genres:
genres += '%s, ' % g
genres = genres[:-2]
self.ui.genresLabel.setText(genres)
else:
self.ui.genresLabel.parent().hide()
if not empty(movie.release_date):
self.ui.releaseDateLabel.setText(movie.release_date.strftime('%Y-%m-%d'))
else:
self.ui.releaseDateLabel.parent().hide()
if not empty(movie.duration):
self.ui.durationLabel.setText('%s min' % movie.duration)
else:
self.ui.durationLabel.parent().hide()
if not empty(movie.pitch):
self.ui.pitchPlain.setPlainText('%s' % movie.pitch)
else:
self.ui.pitchPlain.parent().hide()
if not empty(movie.country):
self.ui.countryLabel.setText('%s' % movie.country)
else:
self.ui.countryLabel.parent().hide()
if not empty(movie.note):
self.ui.noteLabel.setText('%s' % movie.note)
else:
self.ui.noteLabel.parent().hide()
for role in movie.roles.keys():
self.ui.castingCombo.addItem('%s' % role)
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
def putReleases(self):
rel = self.backend.get_movie_releases(self.movie.id)
if not empty(rel):
self.ui.allReleasesPlain.setPlainText(rel)
else:
self.ui.allReleasesPlain.parent().hide()
def gotThumbnail(self):
if not empty(self.movie.thumbnail_url):
data = requests.get(self.movie.thumbnail_url).content
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(220,Qt.SmoothTransformation))
@Slot()
def searchSubtitle(self):
tosearch = unicode(self.movie.original_title)
lang = self.ui.langCombo.currentText()
desc = 'Search subtitles for "%s" (lang:%s)' % (tosearch, lang)
self.parent.doAction(desc, self.parent.searchSubtitleAction, [lang, tosearch])
@Slot()
def searchTorrent(self):
tosearch = self.movie.original_title
if not empty(self.movie.release_date):
tosearch += ' %s' % self.movie.release_date.year
desc = 'Search torrents for "%s"' % tosearch
self.parent.doAction(desc, self.parent.searchTorrentAction, [tosearch])
@Slot()
def casting(self):
role = None
tosearch = self.ui.castingCombo.currentText()
role_desc = ''
if tosearch != 'all':
role = tosearch
role_desc = ' as %s' % role
self.parent.doAction('Casting%s of movie "%s"' % (role_desc, self.movie.original_title),
self.parent.castingAction, [self.backend.name, self.movie.id, role])
@Slot()
def personsInCommon(self):
my_id = self.movie.id
my_title = self.movie.original_title
other_id = self.ui.personsInCommonEdit.text().split('@')[0]
other_movie = self.backend.get_movie(other_id)
if other_id == self.movie.id:
QMessageBox.critical(None, self.tr('"Persons in common" error'),
self.tr('Nice try\nThe movies must be different'),
QMessageBox.Ok)
elif not other_movie:
QMessageBox.critical(None, self.tr('"Persons in common" error'),
self.tr('Movie not found: %s' % other_id),
QMessageBox.Ok)
else:
other_title = other_movie.original_title
desc = 'Persons in common %s, %s'%(my_title, other_title)
self.parent.doAction(desc, self.parent.personsInCommonAction, [self.backend.name, my_id, other_id])
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/person.py 0000664 0000000 0000000 00000011040 13204006733 0027736 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests
from PyQt5.QtCore import pyqtSlot as Slot, Qt
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFrame, QApplication, QMessageBox
from weboob.applications.qcineoob.ui.person_ui import Ui_Person
from weboob.capabilities.base import empty
class Person(QFrame):
def __init__(self, person, backend, parent=None):
super(Person, self).__init__(parent)
self.parent = parent
self.ui = Ui_Person()
self.ui.setupUi(self)
self.ui.filmographyButton.clicked.connect(self.filmography)
self.ui.biographyButton.clicked.connect(self.biography)
self.ui.moviesInCommonButton.clicked.connect(self.moviesInCommon)
self.person = person
self.backend = backend
self.gotThumbnail()
self.ui.nameLabel.setText(person.name)
self.ui.idEdit.setText(u'%s@%s' % (person.id, backend.name))
if not empty(person.real_name):
self.ui.realNameLabel.setText('%s' % person.real_name)
else:
self.ui.realNameLabel.parent().hide()
if not empty(person.birth_place):
self.ui.birthPlaceLabel.setText('%s' % person.birth_place)
else:
self.ui.birthPlaceLabel.parent().hide()
if not empty(person.birth_date):
self.ui.birthDateLabel.setText(person.birth_date.strftime('%Y-%m-%d'))
else:
self.ui.birthDateLabel.parent().hide()
if not empty(person.death_date):
self.ui.deathDateLabel.setText(person.death_date.strftime('%Y-%m-%d'))
else:
self.ui.deathDateLabel.parent().hide()
self.ui.shortBioPlain.setPlainText('%s' % person.short_biography)
for role in person.roles.keys():
self.ui.filmographyCombo.addItem(role)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
def gotThumbnail(self):
if not empty(self.person.thumbnail_url):
data = requests.get(self.person.thumbnail_url).content
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(220,Qt.SmoothTransformation))
@Slot()
def filmography(self):
role = None
tosearch = self.ui.filmographyCombo.currentText()
role_desc = ''
if tosearch != 'all':
role = tosearch
role_desc = ' as %s' % role
self.parent.doAction('Filmography of "%s"%s' % (self.person.name, role_desc),
self.parent.filmographyAction, [self.backend.name, self.person.id, role])
@Slot()
def biography(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
self.backend.fill_person(self.person, 'biography')
bio = self.person.biography
self.ui.shortBioPlain.setPlainText(u'%s' % bio)
self.ui.biographyLabel.setText('Full biography:')
self.ui.biographyButton.hide()
QApplication.restoreOverrideCursor()
@Slot()
def moviesInCommon(self):
my_id = self.person.id
my_name = self.person.name
other_id = self.ui.moviesInCommonEdit.text().split('@')[0]
other_person = self.backend.get_person(other_id)
if other_id == self.person.id:
QMessageBox.critical(None, self.tr('"Moviess in common" error'),
self.tr('Nice try\nThe persons must be different'),
QMessageBox.Ok)
elif not other_person:
QMessageBox.critical(None, self.tr('"Movies in common" error'),
self.tr('Person not found: %s' % other_id),
QMessageBox.Ok)
else:
other_name = other_person.name
desc = 'Movies in common %s, %s'%(my_name, other_name)
self.parent.doAction(desc, self.parent.moviesInCommonAction, [self.backend.name, my_id, other_id])
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/qcineoob.py 0000664 0000000 0000000 00000003336 13204006733 0030240 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.cinema import CapCinema
from weboob.capabilities.torrent import CapTorrent
from weboob.capabilities.subtitle import CapSubtitle
from weboob.tools.application.qt5 import QtApplication
from .main_window import MainWindow
class QCineoob(QtApplication):
APPNAME = 'qcineoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Julien Veyssier'
DESCRIPTION = "Qt application allowing to search movies, people, torrent and subtitles."
SHORT_DESCRIPTION = "search movies, people, torrent and subtitles"
CAPS = CapCinema, CapTorrent, CapSubtitle
CONFIG = {'settings': {'backend': '',
'maxresultsnumber': '10',
'showthumbnails': '0'
}
}
def main(self, argv):
self.load_backends([CapCinema, CapTorrent, CapSubtitle])
self.load_config()
main_window = MainWindow(self.config, self.weboob, self)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/subtitle.py 0000664 0000000 0000000 00000006557 13204006733 0030304 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from PyQt5.QtWidgets import QFrame, QFileDialog
from weboob.applications.qcineoob.ui.subtitle_ui import Ui_Subtitle
from weboob.capabilities.base import empty
class Subtitle(QFrame):
def __init__(self, subtitle, backend, parent=None):
super(Subtitle, self).__init__(parent)
self.parent = parent
self.backend = backend
self.ui = Ui_Subtitle()
self.ui.setupUi(self)
self.ui.downloadButton.clicked.connect(self.download)
self.subtitle = subtitle
self.ui.idEdit.setText(u'%s@%s' % (subtitle.id, backend.name))
self.ui.nameLabel.setText(u'%s' % subtitle.name)
if not empty(subtitle.nb_cd):
self.ui.nbcdLabel.setText(u'%s' % subtitle.nb_cd)
else:
self.ui.nbcdLabel.parent().hide()
if not empty(subtitle.language):
self.ui.langLabel.setText(u'%s' % subtitle.language)
else:
self.ui.langLabel.parent().hide()
if not empty(subtitle.description):
self.ui.descriptionPlain.setPlainText(u'%s' % subtitle.description)
else:
self.ui.descriptionPlain.parent().hide()
if not empty(subtitle.url):
self.ui.urlEdit.setText(u'%s' % subtitle.url)
else:
self.ui.downloadButton.setDisabled(True)
self.ui.downloadButton.setText('Impossible to download this subtitle')
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
@Slot()
def download(self):
if not empty(self.subtitle.url):
if self.subtitle.url.endswith('.rar'):
ext = '.rar'
elif self.subtitle.url.endswith('.srt'):
ext = '.srt'
else:
ext = '.zip'
fileDial = QFileDialog(self, 'Save "%s" subtitle file' %
self.subtitle.name, '%s%s' % (self.subtitle.name, ext), 'all files (*)')
fileDial.setAcceptMode(QFileDialog.AcceptSave)
fileDial.setLabelText(QFileDialog.Accept, 'Save subtitle file')
fileDial.setLabelText(QFileDialog.FileName, 'Subtitle file name')
ok = (fileDial.exec_() == 1)
if not ok:
return
result = fileDial.selectedFiles()
if len(result) > 0:
dest = result[0]
data = self.backend.get_subtitle_file(self.subtitle.id)
try:
with open(dest, 'w') as f:
f.write(data)
except IOError as e:
print('Unable to write subtitle file in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/torrent.py 0000664 0000000 0000000 00000007346 13204006733 0030143 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from PyQt5.QtWidgets import QFrame, QFileDialog
from weboob.applications.qcineoob.ui.torrent_ui import Ui_Torrent
from weboob.applications.weboorrents.weboorrents import sizeof_fmt
from weboob.capabilities.base import empty
from weboob.tools.compat import unicode
class Torrent(QFrame):
def __init__(self, torrent, backend, parent=None):
super(Torrent, self).__init__(parent)
self.parent = parent
self.backend = backend
self.ui = Ui_Torrent()
self.ui.setupUi(self)
self.ui.downloadButton.clicked.connect(self.download)
self.torrent = torrent
self.ui.idEdit.setText(u'%s@%s' % (torrent.id, backend.name))
self.ui.nameLabel.setText(u'%s' % torrent.name)
if not empty(torrent.url):
self.ui.urlEdit.setText(u'%s' % torrent.url)
else:
self.ui.urlFrame.hide()
self.ui.downloadButton.setDisabled(True)
if not empty(torrent.magnet):
self.ui.downloadButton.setText(u'Download not available\nbut magnet link provided')
self.ui.downloadButton.setToolTip(u'Use the magnet link')
if not empty(torrent.description):
self.ui.descriptionPlain.setText(u'%s' % torrent.description)
else:
self.ui.descriptionPlain.parent().hide()
if not empty(torrent.files):
files = u''
for f in torrent.files:
files += '%s\n' % f
self.ui.filesPlain.setText(u'%s' % files)
else:
self.ui.filesPlain.parent().hide()
if not empty(torrent.magnet):
self.ui.magnetEdit.setText(u'%s' % torrent.magnet)
else:
self.ui.magnetFrame.hide()
if not empty(torrent.seeders) and not empty(torrent.leechers):
self.ui.seedLeechLabel.setText(u'%s/%s' % (torrent.seeders, torrent.leechers))
if not empty(torrent.size):
self.ui.sizeLabel.setText(u'%s' % sizeof_fmt(torrent.size))
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
@Slot()
def download(self):
fileDial = QFileDialog(self, 'Save "%s" torrent file' % self.torrent.name, '%s.torrent' %
self.torrent.name, 'Torrent file (*.torrent);;all files (*)')
fileDial.setAcceptMode(QFileDialog.AcceptSave)
fileDial.setLabelText(QFileDialog.Accept, 'Save torrent file')
fileDial.setLabelText(QFileDialog.FileName, 'Torrent file name')
ok = (fileDial.exec_() == 1)
if not ok:
return
result = fileDial.selectedFiles()
if len(result) > 0:
dest = result[0]
data = self.backend.get_torrent_file(self.torrent.id)
try:
with open(unicode(dest), 'w') as f:
f.write(data)
except IOError as e:
print('Unable to write .torrent in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026477 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030142 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030576 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/main_window.ui 0000664 0000000 0000000 00000013715 13204006733 0031360 0 ustar 00root root 0000000 0000000
MainWindow00748463QCineoob4QFrame::StyledPanelQFrame::Raised00Search: 451677721575truebackground-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.479904 rgba(255, 0, 0, 255), stop:0.522685 rgba(255, 255, 255, 255), stop:1 rgba(255, 255, 255, 255));stopmoviepersontorrentsubtitle8language<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>max results<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>-1truetrueShow thumbnailsSearch by id:moviepersontorrentsubtitle0074823toolBarTopToolBarAreafalseBackends
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/minimovie.ui 0000664 0000000 0000000 00000012736 13204006733 0031043 0 ustar 00root root 0000000 0000000
MiniMovie00563138FormQFrame::StyledPanelQFrame::Raised9550075trueTitle75trueShort description:0050truefalseTextLabeltrue75trueWhereTextLabelfalse12020View12020View in new tab12020View thumbnailQt::Horizontal40201677721520PushButtontrueQt::Horizontal4020
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/miniperson.ui 0000664 0000000 0000000 00000013073 13204006733 0031225 0 ustar 00root root 0000000 0000000
MiniPerson00594136FormQFrame::StyledPanelQFrame::Raised9550075trueName0050truefalseTextLabeltrue75trueShort descriptionTextLabelfalse75trueWhere012020View12020View in new tab12020View thumbnailQt::Horizontal40201677721520PushButtontrueQt::Horizontal4020
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/minisubtitle.ui0000664 0000000 0000000 00000010305 13204006733 0031545 0 ustar 00root root 0000000 0000000
MiniSubtitle00464136FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueName0050truefalseTextLabeltrue75trueNb CDTextLabel75trueWhere1677721520View in new tab1677721520View1677721520PushButtontrue
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/minitorrent.ui 0000664 0000000 0000000 00000011177 13204006733 0031417 0 ustar 00root root 0000000 0000000
MiniTorrent00464138FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueName0050truefalseTextLabel75trueSeed/leech:TextLabel75trueSize:TextLabel75trueWhere1677721520View1677721520View in new tab1677721520PushButtontrue
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/movie.ui 0000664 0000000 0000000 00000043030 13204006733 0030155 0 ustar 00root root 0000000 0000000
Movie00655733001000010000FrameQFrame::StyledPanelQFrame::Raised167772153000QFrame::NoFrameQFrame::RaisedQFrame::NoFrameQFrame::Raised00view castingallQFrame::NoFrameQFrame::Raised00search torrentsQFrame::NoFrameQFrame::Raised00search subtitlesQFrame::StyledPanelQFrame::RaisedSearch persons in common with00Movie ID :00167772159000QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueTitle:1677721535QFrame::StyledPanelQFrame::Raised75trueDuration:1677721535QFrame::StyledPanelQFrame::Raised75trueRelease date:16777215150QFrame::StyledPanelQFrame::Raised75truePitch:00Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse1677721535QFrame::StyledPanelQFrame::Raised75trueCountry:1677721535QFrame::StyledPanelQFrame::Raised75trueGenres:true1677721535QFrame::StyledPanelQFrame::Raised75trueNote:16777215200QFrame::StyledPanelQFrame::Raised75trueAll release dates:Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse0016777215200QFrame::StyledPanelQFrame::Raised1001677721575trueOther titles:Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/person.ui 0000664 0000000 0000000 00000027303 13204006733 0030351 0 ustar 00root root 0000000 0000000
Person00857629FrameQFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::RaisedQFrame::NoFrameQFrame::Raisedview
filmographyasallQFrame::StyledPanelQFrame::RaisedSearch movies in common with00Person ID :00QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrueQFrame::StyledPanelQFrame::Raised75truename:QFrame::StyledPanelQFrame::Raised75trueBirth date:QFrame::StyledPanelQFrame::Raised75trueReal Name:QFrame::StyledPanelQFrame::Raised75trueBirth place:QFrame::StyledPanelQFrame::Raised75trueDeath date:QFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::Raised75trueShort biography:view full
biography0016777215250Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/result.ui 0000664 0000000 0000000 00000011173 13204006733 0030357 0 ustar 00root root 0000000 0000000
Result00645733001000010000FrameQFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::Raised00Qt::Horizontal40206020<<back75trueQt::AlignCentertrueQt::Horizontal4020true00605667Qt::Vertical2040true00605667
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/subtitle.ui 0000664 0000000 0000000 00000020437 13204006733 0030677 0 ustar 00root root 0000000 0000000
Subtitle00629552002000600FrameQFrame::StyledPanelQFrame::Raised16777215600QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue167772151000QFrame::StyledPanelQFrame::Raised75trueName:true167772151000QFrame::StyledPanelQFrame::Raised75trueNb CD:167772151000QFrame::StyledPanelQFrame::Raised75trueLang:16777215200QFrame::StyledPanelQFrame::Raised75trueDescription:167772151201677721540QFrame::StyledPanelQFrame::Raised3375trueUrl:Download
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcineoob/ui/torrent.ui 0000664 0000000 0000000 00000024563 13204006733 0030545 0 ustar 00root root 0000000 0000000
Torrent00629571001000010000FrameQFrame::StyledPanelQFrame::Raised1677721510000QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueName:1677721535QFrame::StyledPanelQFrame::Raised75trueSeed/leech:1677721535QFrame::StyledPanelQFrame::Raised75trueSize:015016777215500QFrame::StyledPanelQFrame::Raised75trueDescription:012016777215200QFrame::StyledPanelQFrame::Raised75trueFiles:1677721540QFrame::StyledPanelQFrame::Raised3375trueUrl:1677721540QFrame::StyledPanelQFrame::Raised3375trueMagnet:Download
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ 0000775 0000000 0000000 00000000000 13204006733 0026241 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/__init__.py 0000664 0000000 0000000 00000000072 13204006733 0030351 0 ustar 00root root 0000000 0000000 from .qcookboob import QCookboob
__all__ = ['QCookboob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/main_window.py 0000664 0000000 0000000 00000026330 13204006733 0031132 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from PyQt5.QtCore import pyqtSlot as Slot, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QFrame, QShortcut
from weboob.capabilities.recipe import CapRecipe
from weboob.tools.application.qt5 import QtMainWindow, QtDo
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.models import BackendListModel
from weboob.tools.application.qt5.search_history import HistoryCompleter
from weboob.applications.qcookboob.ui.main_window_ui import Ui_MainWindow
from weboob.applications.qcookboob.ui.result_ui import Ui_Result
from .minirecipe import MiniRecipe
from .recipe import Recipe
MAX_TAB_TEXT_LENGTH=30
class Result(QFrame):
def __init__(self, weboob, app, parent=None):
super(Result, self).__init__(parent)
self.ui = Ui_Result()
self.ui.setupUi(self)
self.parent = parent
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# action history is composed by the last action and the action list
# An action is a function, a list of arguments and a description string
self.action_history = {'last_action': None, 'action_list': []}
self.ui.backButton.clicked.connect(self.doBack)
self.ui.backButton.setShortcut(QKeySequence('Alt+Left'))
self.ui.backButton.hide()
def doAction(self, description, fun, args):
''' Call fun with args as arguments
and save it in the action history
'''
self.ui.currentActionLabel.setText(description)
if self.action_history['last_action'] is not None:
self.action_history['action_list'].append(self.action_history['last_action'])
self.ui.backButton.setToolTip('%s (Alt+Left)'%self.action_history['last_action']['description'])
self.ui.backButton.show()
self.action_history['last_action'] = {'function': fun, 'args': args, 'description': description}
# manage tab text
mytabindex = self.parent.ui.resultsTab.indexOf(self)
tabtxt = description
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
self.parent.ui.resultsTab.setTabText(mytabindex, tabtxt)
self.parent.ui.resultsTab.setTabToolTip(mytabindex, description)
return fun(*args)
@Slot()
def doBack(self):
''' Go back in action history
Basically call previous function and update history
'''
if len(self.action_history['action_list']) > 0:
todo = self.action_history['action_list'].pop()
self.ui.currentActionLabel.setText(todo['description'])
self.action_history['last_action'] = todo
if len(self.action_history['action_list']) == 0:
self.ui.backButton.hide()
else:
self.ui.backButton.setToolTip(self.action_history['action_list'][-1]['description'])
# manage tab text
mytabindex = self.parent.ui.resultsTab.indexOf(self)
tabtxt = todo['description']
if len(tabtxt) > MAX_TAB_TEXT_LENGTH:
tabtxt = '%s...'%tabtxt[:MAX_TAB_TEXT_LENGTH]
self.parent.ui.resultsTab.setTabText(mytabindex, tabtxt)
self.parent.ui.resultsTab.setTabToolTip(mytabindex, todo['description'])
return todo['function'](*todo['args'])
def processFinished(self):
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
@Slot()
def stopProcess(self):
if self.process is not None:
self.process.stop()
def searchRecipe(self,pattern):
if not pattern:
return
self.doAction(u'Search recipe "%s"' % pattern, self.searchRecipeAction, [pattern])
def searchRecipeAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex())
self.process = QtDo(self.weboob, self.addRecipe, fb=self.processFinished)
self.process.do(self.app._do_complete, self.parent.getCount(), ('title'), 'iter_recipes', pattern, backends=backend_name, caps=CapRecipe)
self.parent.ui.stopButton.show()
def addRecipe(self, recipe):
minirecipe = MiniRecipe(self.weboob, self.weboob[recipe.backend], recipe, self)
self.ui.list_content.layout().insertWidget(self.ui.list_content.layout().count()-1,minirecipe)
self.minis.append(minirecipe)
def displayRecipe(self, recipe, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wrecipe = Recipe(recipe, backend, self)
self.ui.info_content.layout().addWidget(wrecipe)
self.current_info_widget = wrecipe
QApplication.restoreOverrideCursor()
def searchId(self, id):
QApplication.setOverrideCursor(Qt.WaitCursor)
if '@' in id:
backend_name = id.split('@')[1]
id = id.split('@')[0]
else:
backend_name = None
for backend in self.weboob.iter_backends():
if (backend_name and backend.name == backend_name) or not backend_name:
recipe = backend.get_recipe(id)
if recipe:
self.doAction('Recipe "%s"' % recipe.title, self.displayRecipe, [recipe, backend])
QApplication.restoreOverrideCursor()
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# search history is a list of patterns which have been searched
history_path = os.path.join(self.weboob.workdir, 'qcookboob_history')
qc = HistoryCompleter(history_path, self)
qc.load()
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
self.ui.searchEdit.returnPressed.connect(self.search)
self.ui.idEdit.returnPressed.connect(self.searchId)
count = self.config.get('settings', 'maxresultsnumber')
self.ui.countSpin.setValue(int(count))
showT = self.config.get('settings', 'showthumbnails')
self.ui.showTCheck.setChecked(showT == '1')
self.ui.stopButton.hide()
self.ui.actionBackends.triggered.connect(self.backendsConfig)
q = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self)
q.activated.connect(self.close)
n = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageDown), self)
n.activated.connect(self.nextTab)
p = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageUp), self)
p.activated.connect(self.prevTab)
w = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_W), self)
w.activated.connect(self.closeCurrentTab)
l = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_L), self)
l.activated.connect(self.ui.searchEdit.setFocus)
l.activated.connect(self.ui.searchEdit.selectAll)
self.ui.resultsTab.tabCloseRequested.connect(self.closeTab)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapRecipe, ), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
model = BackendListModel(self.weboob)
model.addBackends()
self.ui.backendEdit.setModel(model)
current_backend = self.config.get('settings', 'backend')
idx = self.ui.backendEdit.findData(current_backend)
if idx >= 0:
self.ui.backendEdit.setCurrentIndex(idx)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
def getCount(self):
num = self.ui.countSpin.value()
if num == 0:
return None
else:
return num
@Slot(int)
def closeTab(self, index):
if self.ui.resultsTab.widget(index) != 0:
self.ui.resultsTab.removeTab(index)
@Slot()
def closeCurrentTab(self):
self.closeTab(self.ui.resultsTab.currentIndex())
@Slot()
def prevTab(self):
index = self.ui.resultsTab.currentIndex() - 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
@Slot()
def nextTab(self):
index = self.ui.resultsTab.currentIndex() + 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
def newTab(self, txt, backend, recipe=None):
id = ''
if recipe is not None:
id = recipe.id
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, txt)
new_res.searchId('%s@%s'%(id,backend.NAME))
@Slot()
def search(self):
pattern = self.ui.searchEdit.text()
self.ui.searchEdit.completer().addString(pattern)
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, pattern)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.searchRecipe(pattern)
self.ui.stopButton.clicked.connect(new_res.stopProcess)
@Slot()
def searchId(self):
id = self.ui.idEdit.text()
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, id)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.searchId(id)
def closeEvent(self, ev):
self.config.set('settings', 'backend', self.ui.backendEdit.itemData(
self.ui.backendEdit.currentIndex()))
self.ui.searchEdit.completer().save()
self.config.set('settings', 'maxresultsnumber', self.ui.countSpin.value())
self.config.save()
ev.accept()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/minirecipe.py 0000664 0000000 0000000 00000007240 13204006733 0030742 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests
from PyQt5.QtGui import QIcon, QImage, QPixmap, QPixmapCache
from PyQt5.QtWidgets import QFrame, QApplication
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.applications.qcookboob.ui.minirecipe_ui import Ui_MiniRecipe
from weboob.capabilities.base import empty
class MiniRecipe(QFrame):
def __init__(self, weboob, backend, recipe, parent=None):
super(MiniRecipe, self).__init__(parent)
self.parent = parent
self.ui = Ui_MiniRecipe()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.recipe = recipe
self.ui.titleLabel.setText(recipe.title)
if not empty(recipe.short_description):
if len(recipe.short_description) > 300:
self.ui.shortDescLabel.setText('%s [...]'%recipe.short_description[:300])
else:
self.ui.shortDescLabel.setText(recipe.short_description)
else:
self.ui.shortDescLabel.setText('')
self.ui.backendButton.setText(backend.name)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
if icon_path:
pixmap = QPixmapCache.find(icon_path)
if not pixmap:
pixmap = QPixmap(QImage(icon_path))
self.ui.backendButton.setIcon(QIcon(pixmap))
self.ui.newTabButton.clicked.connect(self.newTabPressed)
self.ui.viewButton.clicked.connect(self.viewPressed)
self.ui.viewThumbnailButton.clicked.connect(self.gotThumbnail)
if self.parent.parent.ui.showTCheck.isChecked():
self.gotThumbnail()
@Slot()
def gotThumbnail(self):
if not empty(self.recipe.thumbnail_url):
data = requests.get(self.recipe.thumbnail_url).content
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToHeight(100,Qt.SmoothTransformation))
@Slot()
def viewPressed(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
recipe = self.backend.get_recipe(self.recipe.id)
if recipe:
self.parent.doAction('Recipe "%s"' %
recipe.title, self.parent.displayRecipe, [recipe, self.backend])
@Slot()
def newTabPressed(self):
recipe = self.backend.get_recipe(self.recipe.id)
self.parent.parent.newTab(u'Recipe "%s"' %
recipe.title, self.backend, recipe=recipe)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 2:
self.gotThumbnail()
elif event.button() == 4:
self.newTabPressed()
else:
self.viewPressed()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/qcookboob.py 0000664 0000000 0000000 00000002730 13204006733 0030573 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.recipe import CapRecipe
from weboob.tools.application.qt5 import QtApplication
from .main_window import MainWindow
class QCookboob(QtApplication):
APPNAME = 'qcookboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-2014 Julien Veyssier'
DESCRIPTION = "Qt application allowing to search recipes."
SHORT_DESCRIPTION = "search recipes"
CAPS = CapRecipe
CONFIG = {'settings': {'backend': '',
'maxresultsnumber': '10'
}
}
def main(self, argv):
self.load_backends([CapRecipe])
self.load_config()
main_window = MainWindow(self.config, self.weboob, self)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/recipe.py 0000664 0000000 0000000 00000010631 13204006733 0030063 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import codecs
import requests
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFrame, QFileDialog
from weboob.applications.qcookboob.ui.recipe_ui import Ui_Recipe
from weboob.capabilities.base import empty
class Recipe(QFrame):
def __init__(self, recipe, backend, parent=None):
super(Recipe, self).__init__(parent)
self.parent = parent
self.ui = Ui_Recipe()
self.ui.setupUi(self)
self.ui.exportButton.clicked.connect(self.export)
self.recipe = recipe
self.backend = backend
self.gotThumbnail()
self.ui.idEdit.setText(u'%s@%s' % (recipe.id, backend.name))
if not empty(recipe.title):
self.ui.titleLabel.setText(recipe.title)
if not empty(recipe.nb_person):
nbstr = '-'.join(str(num) for num in recipe.nb_person)
self.ui.nbPersonLabel.setText(nbstr)
else:
self.ui.nbPersonLabel.parent().hide()
if not empty(recipe.preparation_time):
self.ui.prepTimeLabel.setText('%s min' % recipe.preparation_time)
else:
self.ui.prepTimeLabel.parent().hide()
if not empty(recipe.cooking_time):
self.ui.cookingTimeLabel.setText('%s min' % recipe.cooking_time)
else:
self.ui.cookingTimeLabel.parent().hide()
if not empty(recipe.ingredients):
txt = u''
for ing in recipe.ingredients:
txt += u'* %s\n' % ing
self.ui.ingredientsPlain.setPlainText(u'%s' % txt)
else:
self.ui.ingredientsPlain.parent().hide()
if not empty(recipe.author):
self.ui.authorLabel.setText(u'%s' % recipe.author)
else:
self.ui.authorLabel.parent().hide()
if not empty(recipe.instructions):
self.ui.instructionsPlain.setPlainText(u'%s' % recipe.instructions)
else:
self.ui.instructionsPlain.parent().hide()
if not empty(recipe.comments):
txt = u''
for com in recipe.comments:
txt += u'* %s\n' % com
self.ui.commentsPlain.setPlainText(u'%s' % txt)
else:
self.ui.commentsPlain.parent().hide()
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
def gotThumbnail(self):
if self.recipe.picture_url and not empty(self.recipe.picture_url):
data = requests.get(self.recipe.picture_url).content
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(250, Qt.SmoothTransformation))
@Slot()
def export(self):
fileDial = QFileDialog(self, u'Export "%s" recipe' %
self.recipe.title, u'%s.kreml' % self.recipe.id, 'Krecipe file (*.kreml);;all files (*)')
fileDial.setAcceptMode(QFileDialog.AcceptSave)
fileDial.setLabelText(QFileDialog.Accept, 'Export recipe')
fileDial.setLabelText(QFileDialog.FileName, 'Recipe file name')
ok = (fileDial.exec_() == 1)
if not ok:
return
result = fileDial.selectedFiles()
if len(result) > 0:
dest = result[0]
if not dest.endswith('.kreml'):
dest += '.kreml'
data = self.recipe.toKrecipesXml(author=self.backend.name)
try:
with codecs.open(dest, 'w', 'utf-8') as f:
f.write(data)
except IOError as e:
print(u'Unable to write Krecipe file in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026656 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030321 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030755 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/main_window.ui0000664 0000000 0000000 00000010447 13204006733 0031536 0 ustar 00root root 0000000 0000000
MainWindow00748463QCookboob4QFrame::StyledPanelQFrame::Raised00Search: 3516777215background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.479904 rgba(255, 0, 0, 255), stop:0.522685 rgba(255, 255, 255, 255), stop:1 rgba(255, 255, 255, 255));stop<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>max results<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>-1truetrueShow thumbnailssearch by ID:0074827toolBarTopToolBarAreafalseBackends
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/minirecipe.ui 0000664 0000000 0000000 00000012737 13204006733 0031353 0 ustar 00root root 0000000 0000000
MiniRecipe00578136FormQFrame::StyledPanelQFrame::Raised95500TextLabeltrue0050truefalseTextLabeltrue75trueShort description:75trueWhere75trueTitle12020View12020View in new tab12020View thumbnailQt::Horizontal40201677721520PushButtontrueQt::Horizontal4020
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/recipe.ui 0000664 0000000 0000000 00000032534 13204006733 0030473 0 ustar 00root root 0000000 0000000
Recipe006458550080005000FrameQFrame::StyledPanelQFrame::Raised167772153000QFrame::NoFrameQFrame::Raised167772154000QFrame::NoFrameQFrame::RaisedExport to KRecipesML file1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueTitle:1677721535QFrame::StyledPanelQFrame::Raised75trueAuthor:1677721535QFrame::StyledPanelQFrame::Raised75trueNumber of people:1677721535QFrame::StyledPanelQFrame::Raised75truePreparation time:1677721535QFrame::StyledPanelQFrame::Raised75trueCook time:00167772151000QFrame::StyledPanelQFrame::Raised75trueIngredients:0200Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse00167772151000QFrame::StyledPanelQFrame::Raised1001677721575trueInstructions:0200Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse00167772151000QFrame::StyledPanelQFrame::Raised75trueComments:0100Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qcookboob/ui/result.ui 0000664 0000000 0000000 00000011173 13204006733 0030536 0 ustar 00root root 0000000 0000000
Result00645733001000010000FrameQFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::Raised00Qt::Horizontal40206020<<back75trueQt::AlignCentertrueQt::Horizontal4020true00605667Qt::Vertical2040true00605667
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/ 0000775 0000000 0000000 00000000000 13204006733 0026234 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/__init__.py 0000664 0000000 0000000 00000000072 13204006733 0030344 0 ustar 00root root 0000000 0000000 from .qflatboob import QFlatBoob
__all__ = ['QFlatBoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/main_window.py 0000664 0000000 0000000 00000037023 13204006733 0031126 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtGui import QImage, QPixmap, QIcon, QBrush, QColor
from PyQt5.QtWidgets import QLabel, QListWidgetItem
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.tools.compat import range
from weboob.tools.application.qt5 import QtMainWindow, QtDo, HTMLDelegate
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.capabilities.housing import CapHousing, Query, City
from weboob.capabilities.base import NotLoaded, NotAvailable, empty
from .ui.main_window_ui import Ui_MainWindow
from .query import QueryDialog
class HousingListWidgetItem(QListWidgetItem):
def __init__(self, housing, *args, **kwargs):
super(HousingListWidgetItem, self).__init__(*args, **kwargs)
self.housing = housing
self.read = True
def __lt__(self, other):
return '%s%s' % (self.read, self.housing.price_per_meter) < \
'%s%s' % (other.read, other.housing.price_per_meter)
def setAttrs(self, storage):
text = u'
%s
' % self.housing.title
_area = u'%.0fm²' % self.housing.area if self.housing.area else self.housing.area
text += u'%s — %s — %s%s — %.0f %s/m2 (%s)' % (
self.housing.date.strftime('%Y-%m-%d') if self.housing.date else 'Unknown',
_area,
self.housing.cost,
self.housing.currency,
self.housing.price_per_meter,
self.housing.currency,
self.housing.backend)
text += u' %s' % self.housing.text.strip()
text += u' %s' % storage.get('notes', self.housing.fullid, default='').strip().replace('\n', ' ')
self.setText(text)
if self.housing.fullid not in storage.get('read'):
self.setBackground(QBrush(QColor(200, 200, 255)))
self.read = False
elif self.housing.fullid in storage.get('bookmarks'):
self.setBackground(QBrush(QColor(255, 200, 200)))
elif self.background().color() != QColor(0, 0, 0):
self.setBackground(QBrush())
class MainWindow(QtMainWindow):
def __init__(self, config, storage, weboob, app, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.storage = storage
self.weboob = weboob
self.app = app
self.process = None
self.housing = None
self.displayed_photo_idx = 0
self.process_photo = {}
self.process_bookmarks = {}
self.ui.housingsList.setItemDelegate(HTMLDelegate())
self.ui.housingFrame.hide()
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.ui.queriesList.currentIndexChanged.connect(self.queryChanged)
self.ui.addQueryButton.clicked.connect(self.addQuery)
self.ui.editQueryButton.clicked.connect(self.editQuery)
self.ui.removeQueryButton.clicked.connect(self.removeQuery)
self.ui.bookmarksButton.clicked.connect(self.displayBookmarks)
self.ui.housingsList.currentItemChanged.connect(self.housingSelected)
self.ui.previousButton.clicked.connect(self.previousClicked)
self.ui.nextButton.clicked.connect(self.nextClicked)
self.ui.bookmark.stateChanged.connect(self.bookmarkChanged)
self.reloadQueriesList()
self.refreshHousingsList()
if self.weboob.count_backends() == 0:
self.backendsConfig()
if len(self.config.get('queries')) == 0:
self.addQuery()
def closeEvent(self, event):
self.setHousing(None)
QtMainWindow.closeEvent(self, event)
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapHousing,), self)
if bckndcfg.run():
pass
def reloadQueriesList(self, select_name=None):
self.ui.queriesList.currentIndexChanged.disconnect(self.queryChanged)
self.ui.queriesList.clear()
for name in self.config.get('queries', default={}):
self.ui.queriesList.addItem(name)
if name == select_name:
self.ui.queriesList.setCurrentIndex(len(self.ui.queriesList)-1)
self.ui.queriesList.currentIndexChanged.connect(self.queryChanged)
if select_name is not None:
self.queryChanged()
@Slot()
def removeQuery(self):
name = self.ui.queriesList.itemText(self.ui.queriesList.currentIndex())
queries = self.config.get('queries')
queries.pop(name, None)
self.config.set('queries', queries)
self.config.save()
self.reloadQueriesList()
self.queryChanged()
@Slot()
def editQuery(self):
name = self.ui.queriesList.itemText(self.ui.queriesList.currentIndex())
self.addQuery(name)
@Slot()
def addQuery(self, name=None):
querydlg = QueryDialog(self.weboob, self)
if name is not None:
query = self.config.get('queries', name)
querydlg.ui.nameEdit.setText(name)
querydlg.ui.nameEdit.setEnabled(False)
for c in query['cities']:
city = City(c['id'])
city.backend = c['backend']
city.name = c['name']
item = querydlg.buildCityItem(city)
querydlg.ui.citiesList.addItem(item)
querydlg.ui.typeBox.setCurrentIndex(int(query.get('type', 0)))
querydlg.ui.areaMin.setValue(query['area_min'])
querydlg.ui.areaMax.setValue(query['area_max'])
querydlg.ui.costMin.setValue(query['cost_min'])
querydlg.ui.costMax.setValue(query['cost_max'])
querydlg.selectComboValue(querydlg.ui.nbRooms, query['nb_rooms'])
if querydlg.exec_():
name = querydlg.ui.nameEdit.text()
query = {}
query['type'] = querydlg.ui.typeBox.currentIndex()
query['cities'] = []
for i in range(len(querydlg.ui.citiesList)):
item = querydlg.ui.citiesList.item(i)
city = item.data(Qt.UserRole)
query['cities'].append({'id': city.id, 'backend': city.backend, 'name': city.name})
query['area_min'] = querydlg.ui.areaMin.value()
query['area_max'] = querydlg.ui.areaMax.value()
query['cost_min'] = querydlg.ui.costMin.value()
query['cost_max'] = querydlg.ui.costMax.value()
try:
query['nb_rooms'] = int(querydlg.ui.nbRooms.itemText(querydlg.ui.nbRooms.currentIndex()))
except ValueError:
query['nb_rooms'] = 0
self.config.set('queries', name, query)
self.config.save()
self.reloadQueriesList(name)
@Slot(int)
def queryChanged(self, i=None):
self.refreshHousingsList()
def refreshHousingsList(self):
name = self.ui.queriesList.itemText(self.ui.queriesList.currentIndex())
q = self.config.get('queries', name)
if q is None:
return q
self.ui.housingsList.clear()
self.ui.queriesList.setEnabled(False)
self.ui.bookmarksButton.setEnabled(False)
query = Query()
query.type = int(q.get('type', 0))
query.cities = []
for c in q['cities']:
city = City(c['id'])
city.backend = c['backend']
city.name = c['name']
query.cities.append(city)
query.area_min = int(q['area_min']) or None
query.area_max = int(q['area_max']) or None
query.cost_min = int(q['cost_min']) or None
query.cost_max = int(q['cost_max']) or None
query.nb_rooms = int(q['nb_rooms']) or None
self.process = QtDo(self.weboob, self.addHousing, fb=self.addHousingEnd)
self.process.do(self.app._do_complete, 20, None, 'search_housings', query)
@Slot()
def displayBookmarks(self):
self.ui.housingsList.clear()
self.ui.queriesList.setEnabled(False)
self.ui.queriesList.setCurrentIndex(-1)
self.ui.bookmarksButton.setEnabled(False)
self.processes = {}
for id in self.storage.get('bookmarks'):
_id, backend_name = id.rsplit('@', 1)
self.process_bookmarks[id] = QtDo(self.weboob, self.addHousing, fb=self.addHousingEnd)
self.process_bookmarks[id].do('get_housing', _id, backends=backend_name)
def addHousingEnd(self):
self.ui.queriesList.setEnabled(True)
self.ui.bookmarksButton.setEnabled(True)
self.process = None
def addHousing(self, housing):
if not housing:
return
item = HousingListWidgetItem(housing)
item.setAttrs(self.storage)
if housing.photos is NotLoaded:
process = QtDo(self.weboob, lambda c: self.setPhoto(c, item))
process.do('fillobj', housing, ['photos'], backends=housing.backend)
self.process_photo[housing.id] = process
elif housing.photos is not NotAvailable and len(housing.photos) > 0:
if not self.setPhoto(housing, item):
photo = housing.photos[0]
process = QtDo(self.weboob, lambda p: self.setPhoto(housing, item))
process.do('fillobj', photo, ['data'], backends=housing.backend)
self.process_photo[housing.id] = process
self.ui.housingsList.addItem(item)
if housing.fullid in self.process_bookmarks:
self.process_bookmarks.pop(housing.fullid)
@Slot(QListWidgetItem, QListWidgetItem)
def housingSelected(self, item, prev):
if item is not None:
housing = item.housing
self.ui.queriesFrame.setEnabled(False)
read = set(self.storage.get('read'))
read.add(housing.fullid)
self.storage.set('read', list(read))
self.storage.save()
self.process = QtDo(self.weboob, self.gotHousing)
self.process.do('fillobj', housing, backends=housing.backend)
else:
housing = None
self.setHousing(housing)
if prev:
prev.setAttrs(self.storage)
def setPhoto(self, housing, item):
if not housing:
return False
try:
self.process_photo.pop(housing.id, None)
except KeyError:
pass
if not housing.photos:
return False
img = None
for photo in housing.photos:
if photo.data:
img = QImage.fromData(photo.data)
break
if img:
item.setIcon(QIcon(QPixmap.fromImage(img)))
return True
return False
def setHousing(self, housing, nottext='Loading...'):
if self.housing is not None:
self.saveNotes()
self.housing = housing
if self.housing is None:
self.ui.housingFrame.hide()
return
self.ui.housingFrame.show()
self.display_photo()
self.ui.bookmark.setChecked(housing.fullid in self.storage.get('bookmarks'))
self.ui.titleLabel.setText('
%s
' % housing.title)
_area = u'%.2f m²' % housing.area if housing.area else housing.area
self.ui.areaLabel.setText(u'%s' % _area)
self.ui.costLabel.setText(u'%s %s' % (housing.cost, housing.currency))
self.ui.pricePerMeterLabel.setText(u'%.2f %s/m²' % (housing.price_per_meter, housing.currency))
self.ui.dateLabel.setText(housing.date.strftime('%Y-%m-%d') if housing.date else nottext)
self.ui.phoneLabel.setText(housing.phone or nottext)
self.ui.locationLabel.setText(housing.location or nottext)
self.ui.stationLabel.setText(housing.station or nottext)
self.ui.urlLabel.setText('%s' % (housing.url or nottext, housing.url or nottext))
text = housing.text.replace('\n', ' ') if housing.text else nottext
self.ui.descriptionEdit.setText(text)
self.ui.notesEdit.setText(self.storage.get('notes', housing.fullid, default=''))
while self.ui.detailsFrame.layout().count() > 0:
child = self.ui.detailsFrame.layout().takeAt(0)
child.widget().hide()
child.widget().deleteLater()
if housing.details:
for key, value in housing.details.items():
if empty(value):
continue
label = QLabel(value)
label.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)
self.ui.detailsFrame.layout().addRow('%s:' % key, label)
def gotHousing(self, housing):
self.setHousing(housing, nottext='')
self.ui.queriesFrame.setEnabled(True)
self.process = None
@Slot(int)
def bookmarkChanged(self, state):
bookmarks = set(self.storage.get('bookmarks'))
if state == Qt.Checked:
bookmarks.add(self.housing.fullid)
elif self.housing.fullid in bookmarks:
bookmarks.remove(self.housing.fullid)
self.storage.set('bookmarks', list(bookmarks))
self.storage.save()
def saveNotes(self):
if not self.housing:
return
txt = self.ui.notesEdit.toPlainText().strip()
if len(txt) > 0:
self.storage.set('notes', self.housing.fullid, txt)
else:
self.storage.delete('notes', self.housing.fullid)
self.storage.save()
@Slot()
def previousClicked(self):
if not self.housing.photos or len(self.housing.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx - 1) % len(self.housing.photos)
self.display_photo()
@Slot()
def nextClicked(self):
if not self.housing.photos or len(self.housing.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx + 1) % len(self.housing.photos)
self.display_photo()
def display_photo(self):
if not self.housing.photos:
self.ui.photosFrame.hide()
return
if self.displayed_photo_idx >= len(self.housing.photos):
self.displayed_photo_idx = len(self.housing.photos) - 1
if self.displayed_photo_idx < 0:
self.ui.photosFrame.hide()
return
self.ui.photosFrame.show()
photo = self.housing.photos[self.displayed_photo_idx]
if photo.data:
data = photo.data
if photo.id in self.process_photo:
self.process_photo.pop(photo.id)
else:
self.process_photo[photo.id] = QtDo(self.weboob, lambda p: self.display_photo())
self.process_photo[photo.id].do('fillobj', photo, ['data'], backends=self.housing.backend)
return
img = QImage.fromData(data)
img = img.scaledToWidth(self.width()/3)
self.ui.photoLabel.setPixmap(QPixmap.fromImage(img))
if photo.url is not NotLoaded:
text = '%s' % (photo.url, photo.url)
self.ui.photoUrlLabel.setText(text)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/qflatboob.py 0000664 0000000 0000000 00000003027 13204006733 0030561 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.housing import CapHousing
from weboob.tools.application.qt5 import QtApplication
from weboob.tools.config.yamlconfig import YamlConfig
from .main_window import MainWindow
class QFlatBoob(QtApplication):
APPNAME = 'qflatboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-2014 Romain Bignon'
DESCRIPTION = "Qt application to search for housing."
SHORT_DESCRIPTION = "search for housing"
CAPS = CapHousing
CONFIG = {'queries': {}}
STORAGE = {'bookmarks': [], 'read': [], 'notes': {}}
def main(self, argv):
self.load_backends(CapHousing)
self.create_storage()
self.load_config(klass=YamlConfig)
main_window = MainWindow(self.config, self.storage, self.weboob, self)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/query.py 0000664 0000000 0000000 00000007152 13204006733 0027760 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QDialog, QListWidgetItem, QMessageBox
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.tools.application.qt5 import QtDo, HTMLDelegate
from weboob.tools.compat import range
from .ui.query_ui import Ui_QueryDialog
class QueryDialog(QDialog):
def __init__(self, weboob, parent=None):
super(QueryDialog, self).__init__(parent)
self.ui = Ui_QueryDialog()
self.ui.setupUi(self)
self.weboob = weboob
self.ui.resultsList.setItemDelegate(HTMLDelegate())
self.ui.citiesList.setItemDelegate(HTMLDelegate())
self.search_process = None
self.ui.cityEdit.returnPressed.connect(self.searchCity)
self.ui.resultsList.itemDoubleClicked.connect(self.insertCity)
self.ui.citiesList.itemDoubleClicked.connect(self.removeCity)
self.ui.buttonBox.accepted.connect(self.okButton)
if hasattr(self.ui.cityEdit, "setPlaceholderText"):
self.ui.cityEdit.setPlaceholderText("Press enter to search city")
def keyPressEvent(self, event):
"""
Disable handler and to prevent closing the window.
"""
event.ignore()
def selectComboValue(self, box, value):
for i in range(box.count()):
if box.itemText(i) == str(value):
box.setCurrentIndex(i)
break
@Slot()
def searchCity(self):
pattern = self.ui.cityEdit.text()
self.ui.resultsList.clear()
self.ui.cityEdit.clear()
self.ui.cityEdit.setEnabled(False)
self.search_process = QtDo(self.weboob, self.addResult, fb=self.addResultEnd)
self.search_process.do('search_city', pattern)
def addResultEnd(self):
self.search_process = None
self.ui.cityEdit.setEnabled(True)
def addResult(self, city):
if not city:
return
item = self.buildCityItem(city)
self.ui.resultsList.addItem(item)
self.ui.resultsList.sortItems()
def buildCityItem(self, city):
item = QListWidgetItem()
item.setText('%s (%s)' % (city.name, city.backend))
item.setData(Qt.UserRole, city)
return item
@Slot(QListWidgetItem)
def insertCity(self, i):
item = QListWidgetItem()
item.setText(i.text())
item.setData(Qt.UserRole, i.data(Qt.UserRole))
self.ui.citiesList.addItem(item)
@Slot(QListWidgetItem)
def removeCity(self, item):
self.ui.citiesList.removeItemWidget(item)
@Slot()
def okButton(self):
if not self.ui.nameEdit.text():
QMessageBox.critical(self, self.tr('Error'), self.tr('Please enter a name to your query.'), QMessageBox.Ok)
return
if self.ui.citiesList.count() == 0:
QMessageBox.critical(self, self.tr('Error'), self.tr('Please add at least one city.'), QMessageBox.Ok)
return
self.accept()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026651 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030314 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030750 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/ui/main_window.ui0000664 0000000 0000000 00000050215 13204006733 0031526 0 ustar 00root root 0000000 0000000
MainWindow00709572QFlatBoobQt::Horizontal00QFrame::StyledPanelQFrame::Raised+edit-00QAbstractItemView::NoEditTriggers128128trueBookmarksQt::Horizontal10QFrame::StyledPanelQFrame::RaisedQt::Vertical00200<h1>Loading...</h1>QFormLayout::ExpandingFieldsGrow75trueCostLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse75trueArea75truePrice per square meter75trueDate75truePhone75trueLocation75trueStation75trueURLLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::VerticalQSizePolicy::Expanding205QFrame::StyledPanelQFrame::Raised002016777215<Qt::AlignCenter002016777215>Qt::AlignCenterQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::VerticalQSizePolicy::Expanding135Qt::HorizontalQt::Vertical050true00falseQFrame::NoFrameQFrame::PlainQFormLayout::ExpandingFieldsGrow200070924toolBarTopToolBarAreafalseBackendsaddQueryButtonqueriesListeditQueryButtonremoveQueryButtonhousingsListbookmarksButtonbookmarkpreviousButtonnextButtondescriptionEdit
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qflatboob/ui/query.ui 0000664 0000000 0000000 00000021441 13204006733 0030357 0 ustar 00root root 0000000 0000000
QueryDialog00551457Add a queryQFrame::StyledPanelQFrame::RaisedName of this query:RentSaleCitiesNumber of roomsN/A12345AreaMinMax99995m²99995m²CostMinMax99999999100€99999999100€Qt::HorizontalQDialogButtonBox::Cancel|QDialogButtonBox::OknameEditcityEditresultsListcitiesListnbRoomsareaMinareaMaxcostMincostMaxbuttonBoxbuttonBoxrejected()QueryDialogreject()316260286274
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/ 0000775 0000000 0000000 00000000000 13204006733 0026412 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/__init__.py 0000664 0000000 0000000 00000000075 13204006733 0030525 0 ustar 00root root 0000000 0000000 from .qgalleroob import QGalleroob
__all__ = ['QGalleroob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/main_window.py 0000664 0000000 0000000 00000020467 13204006733 0031310 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from PyQt5.QtCore import Qt, QModelIndex, pyqtSlot as Slot
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import QApplication
from weboob.tools.application.qt5 import QtMainWindow
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.models import BackendListModel, ResultModel, FilterTypeModel
from weboob.tools.compat import range
from weboob.capabilities.collection import BaseCollection, CapCollection
from weboob.capabilities.gallery import CapGallery, BaseGallery
from weboob.capabilities.image import CapImage, BaseImage
from .ui.mainwindow_ui import Ui_MainWindow
from .viewer import Viewer
def size_format(n):
UNITS = [
(1 << 40, 'TiB'),
(1 << 30, 'GiB'),
(1 << 20, 'MiB'),
(1 << 10, 'KiB'),
(0, 'B')
]
for f, u in UNITS:
if n > f:
return '%.2f %s' % (n / float(f), u)
class MainWindow(QtMainWindow):
def __init__(self, config, storage, weboob, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.mdl = ResultModel(weboob)
self.mdl.setColumnFields([['name', 'title'],['url']])
self.mdl.jobAdded.connect(self._jobAdded)
self.mdl.jobFinished.connect(self._jobFinished)
self.proxy_mdl = FilterTypeModel()
self.proxy_mdl.setAcceptedTypes([BaseCollection, BaseGallery])
self.proxy_mdl.setSourceModel(self.mdl)
self.ui.collectionTree.setModel(self.proxy_mdl)
self.ui.collectionTree.selectionModel().currentChanged.connect(self.showCollection)
n = self.mdl.columnCount(QModelIndex())
for i in range(n):
self.ui.collectionTree.setColumnWidth(i, self.ui.collectionTree.width() // n)
self.config = config
self.storage = storage
self.weboob = weboob
self.ui.browseButton.clicked.connect(self.startBrowse)
self.ui.searchGallEdit.returnPressed.connect(self.startGallSearch)
self.ui.searchGallButton.clicked.connect(self.startGallSearch)
self.ui.searchImgEdit.returnPressed.connect(self.startImgSearch)
self.ui.searchImgButton.clicked.connect(self.startImgSearch)
self.ui.imageList.setModel(self.mdl)
self.ui.imageList.selectionModel().currentChanged.connect(self.showImageInfo)
self.ui.imageList.activated.connect(self.openImage)
self.fillBackends()
if self.weboob.count_backends() == 0:
self.backendsConfig()
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.ui.limitResults.valueChanged.connect(self._limitResultsChanged)
self.mdl.setLimit(self.ui.limitResults.value())
self.lastSaveDir = os.path.expanduser('~')
@Slot()
def backendsConfig(self):
cfg = BackendCfg(self.weboob, (CapImage,), self)
if cfg.run():
self.fillBackends()
def fillBackends(self):
model = BackendListModel(self.weboob)
model.addBackends(CapGallery)
self.ui.backendGallCombo.setModel(model)
model = BackendListModel(self.weboob)
model.addBackends(CapImage)
self.ui.backendImgCombo.setModel(model)
model = BackendListModel(self.weboob)
model.addBackends(CapImage, entry_title=True)
model.addBackends(CapGallery, entry_title=True)
self.ui.backendCollCombo.setModel(model)
@Slot()
def changeBackend(self):
cap, backend = self.selectedBackend()
if cap is CapImage:
res_class = BaseImage
else:
res_class = BaseGallery
self.mdl.setResourceClasses([res_class])
@Slot(QModelIndex)
def showCollection(self, qidx):
qidx = self.proxy_mdl.mapToSource(qidx)
qidx = qidx.sibling(qidx.row(), 0)
self.ui.imageList.setRootIndex(qidx)
@Slot(QModelIndex)
def showGallery(self, qidx):
self.ui.imageList.setEnabled(True)
self.ui.imageList.show()
qidx = qidx.sibling(qidx.row(), 0)
self.ui.imageList.setRootIndex(qidx)
def showNoneItem(self):
self.ui.labelTitle.setText('-')
self.ui.labelDescription.setText('-')
self.ui.labelAuthor.setText('-')
self.ui.labelDate.setText('-')
self.ui.labelLink.setText('-')
self.ui.labelSize.setText('-')
self.ui.labelRating.setText('-')
@Slot(QModelIndex)
def showImageInfo(self, qidx):
obj = qidx.data(ResultModel.RoleObject)
if obj is None:
self.showNoneItem()
return
if isinstance(obj, BaseImage):
self.ui.labelTitle.setText(obj.title or '')
self.ui.labelDescription.setText(obj.description or '')
self.ui.labelAuthor.setText(obj.author or '')
if obj.size:
self.ui.labelSize.setText(size_format(obj.size) or '')
else:
self.ui.labelSize.setText('-')
if obj.url:
self.ui.labelLink.setText('Link' % obj.url)
else:
self.ui.labelLink.setText('')
elif isinstance(obj, BaseGallery):
pass
@Slot(QModelIndex)
def openImage(self, qidx):
obj = qidx.data(ResultModel.RoleObject)
if isinstance(obj, BaseImage):
viewer = Viewer(self.weboob, self)
viewer.jobAdded.connect(self._jobAdded)
viewer.jobFinished.connect(self._jobFinished)
viewer.setData(self.mdl, qidx)
viewer.show()
elif isinstance(obj, BaseGallery):
self.ui.imageList.setRootIndex(qidx)
qidx = self.proxy_mdl.mapFromSource(qidx)
self.ui.collectionTree.setCurrentIndex(qidx)
@Slot()
def startImgSearch(self):
backend = self.ui.backendImgCombo.currentData(BackendListModel.RoleBackendName)
if not backend:
backend = list(self.weboob.iter_backends(caps=CapImage))
pattern = self.ui.searchImgEdit.text()
if not pattern:
return
self.mdl.clear()
self.ui.imageList.setRootIndex(QModelIndex())
self.ui.collectionTree.setRootIndex(QModelIndex())
self.mdl.addRootDoLimit(BaseImage, 'search_image', pattern, backends=backend)
@Slot()
def startGallSearch(self):
backend = self.ui.backendGallCombo.currentData(BackendListModel.RoleBackendName)
if not backend:
backend = list(self.weboob.iter_backends(caps=CapGallery))
pattern = self.ui.searchGallEdit.text()
if not pattern:
return
self.mdl.clear()
self.ui.imageList.setRootIndex(QModelIndex())
self.ui.collectionTree.setRootIndex(QModelIndex())
self.mdl.addRootDoLimit(BaseGallery, 'search_galleries', pattern, backends=backend)
@Slot()
def startBrowse(self):
self.mdl.clear()
cap = self.ui.backendCollCombo.currentData(BackendListModel.RoleCapability)
backend = self.ui.backendCollCombo.currentData(BackendListModel.RoleBackendName)
if backend:
backends = [backend]
else:
backends = [b for b in self.weboob.iter_backends(caps=[cap]) if b.has_caps(CapCollection)]
res_classes = [BaseImage, BaseGallery]
self.mdl.setResourceClasses(res_classes)
self.mdl.addRootDoLimit(None, 'iter_resources', res_classes, [], backends=backends)
@Slot()
def _jobAdded(self):
QApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
@Slot()
def _jobFinished(self):
QApplication.restoreOverrideCursor()
@Slot(int)
def _limitResultsChanged(self, value):
self.mdl.setLimit(value)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/qgalleroob.py 0000664 0000000 0000000 00000003107 13204006733 0031114 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.gallery import CapGallery
from weboob.capabilities.image import CapImage
from weboob.tools.application.qt5 import QtApplication
from weboob.tools.config.yamlconfig import YamlConfig
from .main_window import MainWindow
class QGalleroob(QtApplication):
APPNAME = 'qgalleroob'
VERSION = '1.4'
COPYRIGHT = u'Copyright(C) 2016 Vincent A'
DESCRIPTION = "Qt application to view image galleries."
SHORT_DESCRIPTION = "search for images"
#~ CONFIG = {'queries': {}}
#~ STORAGE = {'bookmarks': [], 'read': [], 'notes': {}}
def main(self, argv):
self.load_backends(CapGallery)
self.load_backends(CapImage)
self.create_storage()
self.load_config(klass=YamlConfig)
main_window = MainWindow(self.config, self.storage, self.weboob)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/ui/ 0000775 0000000 0000000 00000000000 13204006733 0027027 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030472 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0031126 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/ui/mainwindow.ui0000664 0000000 0000000 00000036562 13204006733 0031556 0 ustar 00root root 0000000 0000000
MainWindow001024768QGalleroob256256QAbstractItemView::ScrollPerPixelQAbstractItemView::ScrollPerPixelQListView::StaticQListView::LeftToRighttrueQListView::Adjust275275QListView::IconModetrue00102419ToolBarfalsefalseTopToolBarAreafalseQDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovableInfo2QFrame::StyledPanelQFrame::Raised75trueTitle75trueDescription75trueAuthor-Qt::PlainText-true-Qt::PlainText-truetrue75trueDate-Qt::PlainTextQt::Vertical204075trueSize75trueRating-Qt::PlainText-Qt::PlainTexttrueQDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovableFind1Results limit:Qt::Horizontal4020No limit1000050000256214Search (galleries)Text to search...GoQt::Vertical204000256214Search (images)Text to search...GoQt::Vertical204000256214BrowseStart browsing collectionsQt::Horizontal4020Qt::Vertical2040QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovableResults1true4848true0BackendsDownload gallery...Save image...Download to CBZ...
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/ui/viewer.ui 0000664 0000000 0000000 00000013425 13204006733 0030674 0 ustar 00root root 0000000 0000000
Viewer00800600QGalleroob viewertrueQt::AlignCenter007805030000Qt::AlignCenterQt::Horizontal4020|<<1/N>>|Qt::Horizontal4020toolBarTopToolBarAreafalse0080019FileCloseSave image...Original sizeGFit to windowFZoom out-Zoom in+
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qgalleroob/viewer.py 0000664 0000000 0000000 00000016567 13204006733 0030304 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import re
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtGui import QPixmap, QImage, QKeySequence
from PyQt5.QtCore import Qt, pyqtSlot as Slot, pyqtSignal as Signal, QModelIndex, QPersistentModelIndex
from weboob.tools.application.qt5 import QtMainWindow
from weboob.tools.application.qt5.models import ResultModel
from weboob.capabilities.base import NotLoaded
from .ui.viewer_ui import Ui_Viewer
ZOOM_FACTOR, ZOOM_FIT = range(2)
class Viewer(QtMainWindow):
jobAdded = Signal()
jobFinished = Signal()
def __init__(self, weboob, parent=None):
super(Viewer, self).__init__(parent)
self.ui = Ui_Viewer()
self.ui.setupUi(self)
self.ui.prevButton.clicked.connect(self.prev)
self.ui.nextButton.clicked.connect(self.next)
self.ui.firstButton.clicked.connect(self.first)
self.ui.lastButton.clicked.connect(self.last)
self.ui.actionZoomIn.triggered.connect(self.zoomIn)
self.ui.actionZoomOut.triggered.connect(self.zoomOut)
self.ui.actionFullSize.triggered.connect(self.zoomFullSize)
self.ui.actionFitWindow.triggered.connect(self.zoomFit)
self.ui.actionSaveImage.setShortcut(QKeySequence.Save)
self.ui.actionSaveImage.triggered.connect(self.saveImage)
self.ui.actionClose.setShortcut(QKeySequence.Close)
self.ui.actionClose.triggered.connect(self.close)
self.model = None
self.current = None
self.total = 0
self.zoomFactor = 1
self.zoomMode = ZOOM_FACTOR
self.weboob = weboob
def setData(self, model, qidx):
self.model = model
self.current = QPersistentModelIndex(qidx)
self.model.rowsInserted.connect(self.updatePos)
self.model.rowsRemoved.connect(self.updatePos)
self.model.rowsInserted.connect(self.updateNavButtons)
self.model.rowsRemoved.connect(self.updateNavButtons)
self.model.dataChanged.connect(self._dataChanged)
self.model.modelReset.connect(self.disable)
self.updateImage()
@Slot()
def disable(self):
self.setEnabled(False)
def updateNavButtons(self):
prev = self.current.row() > 0
self.ui.prevButton.setEnabled(prev)
self.ui.firstButton.setEnabled(prev)
next = self.current.row() < self.total - 1
self.ui.nextButton.setEnabled(next)
self.ui.lastButton.setEnabled(next)
def updatePos(self):
self.total = self.model.rowCount(self.current.parent())
self.ui.posLabel.setText('%d / %d' % (self.current.row() + 1, self.total))
def updateImage(self):
self.updatePos()
self.updateNavButtons()
obj = self.current.data(ResultModel.RoleObject)
if obj.data is NotLoaded:
self.model.fillObj(obj, ['data'], QModelIndex(self.current))
self.pixmap = None
elif obj.data:
self.pixmap = QPixmap(QImage.fromData(obj.data))
else:
self.pixmap = QPixmap()
self._rebuildImage()
@Slot(QModelIndex)
def _dataChanged(self, qidx):
if qidx == self.current:
obj = qidx.data(ResultModel.RoleObject)
if obj.data:
self.pixmap = QPixmap(QImage.fromData(obj.data))
else:
self.pixmap = QPixmap()
self._rebuildImage()
@Slot()
def next(self):
new = self.current.sibling(self.current.row() + 1, 0)
if not new.isValid():
return
self.current = QPersistentModelIndex(new)
self.updateImage()
@Slot()
def prev(self):
if self.current.row() == 0:
return
self.current = QPersistentModelIndex(self.current.sibling(self.current.row() - 1, 0))
self.updateImage()
@Slot()
def first(self):
self.current = QPersistentModelIndex(self.current.sibling(0, 0))
self.updateImage()
@Slot()
def last(self):
self.current = QPersistentModelIndex(self.current.sibling(self.total - 1, 0))
self.updateImage()
@Slot()
def zoomIn(self):
self.zoomFactor *= 1.25
self.zoomMode = ZOOM_FACTOR
self._rebuildImage()
@Slot()
def zoomOut(self):
self.zoomFactor *= 0.75
self.zoomMode = ZOOM_FACTOR
self._rebuildImage()
@Slot()
def zoomFullSize(self):
self.zoomFactor = 1
self.zoomMode = ZOOM_FACTOR
self._rebuildImage()
@Slot()
def zoomFit(self):
self.zoomMode = ZOOM_FIT
self._rebuildImage()
def resizeEvent(self, ev):
super(Viewer, self).resizeEvent(ev)
if self.zoomMode == ZOOM_FIT:
self._rebuildImage()
def _rebuildZoom(self):
if self.zoomMode == ZOOM_FACTOR:
new_width = int(self.pixmap.width() * self.zoomFactor)
pixmap = self.pixmap.scaledToWidth(new_width, Qt.SmoothTransformation)
else:
new_size = self.ui.scrollArea.viewport().size()
pixmap = self.pixmap.scaled(new_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.zoomFactor = pixmap.width() / float(self.pixmap.width())
return pixmap
def _rebuildImage(self):
if self.pixmap is None:
self.ui.view.setText('Loading...')
return
elif self.pixmap.isNull():
self.ui.view.setText('Image could not be loaded')
return
pixmap = self._rebuildZoom()
self.ui.view.setPixmap(pixmap)
@Slot()
def saveImage(self):
def ext_for_filter(s):
return re.match(r'(?:[A-Z]+) \(\*\.([a-z]+)\)$', s).group(1)
if not self.pixmap:
return
filters = ['PNG (*.png)', 'JPEG (*.jpg)', 'GIF (*.gif)']
obj = self.current.data(ResultModel.RoleObject)
name = '%s.%s' % (obj.title or obj.id or u'', obj.ext or 'png')
default = filters[0]
for f in filters:
if name.endswith(ext_for_filter(f)):
default = f
filters = ';;'.join(filters)
target = os.path.join(self.parent().lastSaveDir, name)
out, filter = QFileDialog.getSaveFileName(self, 'Save image', target, filters, default)
if not out:
return
ext = ext_for_filter(filter)
self.parent().lastSaveDir = os.path.dirname(out)
if not os.path.splitext(out)[1]:
out = '%s.%s' % (out, ext)
if os.path.exists(out):
q = self.tr('%s already exists, are you sure you want to replace it?') % out
reply = QMessageBox.question(self, self.tr('Overwrite?'), q)
if reply == QMessageBox.No:
return self.saveImage()
self.pixmap.save(out, ext.upper())
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/ 0000775 0000000 0000000 00000000000 13204006733 0026230 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/__init__.py 0000664 0000000 0000000 00000000072 13204006733 0030340 0 ustar 00root root 0000000 0000000 from .qhandjoob import QHandJoob
__all__ = ['QHandJoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/main_window.py 0000664 0000000 0000000 00000015136 13204006733 0031123 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Sébastien Monel
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QListWidgetItem, QApplication
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.tools.application.qt5 import QtMainWindow, QtDo
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.search_history import HistoryCompleter
from weboob.capabilities.job import CapJob
from .ui.main_window_ui import Ui_MainWindow
import os
class JobListWidgetItem(QListWidgetItem):
def __init__(self, job, *args, **kwargs):
super(JobListWidgetItem, self).__init__(*args, **kwargs)
self.job = job
def __lt__(self, other):
if self.job.publication_date and other.job.publication_date:
return self.job.publication_date < other.job.publication_date
else:
return False
def setAttrs(self, storage):
text = u'%s - %s' % (self.job.backend, self.job.title)
self.setText(text)
class MainWindow(QtMainWindow):
def __init__(self, config, storage, weboob, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.storage = storage
self.weboob = weboob
self.process = None
self.displayed_photo_idx = 0
self.process_photo = {}
self.process_bookmarks = {}
# search history is a list of patterns which have been searched
history_path = os.path.join(self.weboob.workdir, 'qhandjoob_history')
qc = HistoryCompleter(history_path, self)
qc.load()
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
self.ui.jobFrame.hide()
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.ui.searchEdit.returnPressed.connect(self.doSearch)
self.ui.jobList.currentItemChanged.connect(self.jobSelected)
self.ui.searchButton.clicked.connect(self.doSearch)
self.ui.refreshButton.clicked.connect(self.doAdvancedSearch)
self.ui.queriesTabWidget.currentChanged.connect(self.tabChange)
self.ui.jobListAdvancedSearch.currentItemChanged.connect(self.jobSelected)
self.ui.idEdit.returnPressed.connect(self.openJob)
if self.weboob.count_backends() == 0:
self.backendsConfig()
@Slot(int)
def tabChange(self, index):
if index == 1:
self.doAdvancedSearch()
def searchFinished(self):
self.process = None
QApplication.restoreOverrideCursor()
@Slot()
def doAdvancedSearch(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
self.ui.jobListAdvancedSearch.clear()
self.process = QtDo(self.weboob, self.addJobAdvancedSearch, fb=self.searchFinished)
self.process.do('advanced_search_job')
@Slot()
def doSearch(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
pattern = self.ui.searchEdit.text()
self.ui.searchEdit.completer().addString(pattern)
self.ui.jobList.clear()
self.process = QtDo(self.weboob, self.addJobSearch, fb=self.searchFinished)
self.process.do('search_job', pattern)
def addJobSearch(self, job):
item = self.addJob(job)
if item:
self.ui.jobList.addItem(item)
def addJobAdvancedSearch(self, job):
item = self.addJob(job)
if item:
self.ui.jobListAdvancedSearch.addItem(item)
def addJob(self, job):
if not job:
return
item = JobListWidgetItem(job)
item.setAttrs(self.storage)
return item
def closeEvent(self, event):
self.ui.searchEdit.completer().save()
QtMainWindow.closeEvent(self, event)
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapJob,), self)
if bckndcfg.run():
pass
@Slot(QListWidgetItem, QListWidgetItem)
def jobSelected(self, item, prev):
QApplication.setOverrideCursor(Qt.WaitCursor)
if item is not None:
job = item.job
self.ui.queriesTabWidget.setEnabled(False)
self.process = QtDo(self.weboob, self.gotJob)
self.process.do('fillobj', job, backends=job.backend)
else:
job = None
self.setJob(job)
if prev:
prev.setAttrs(self.storage)
@Slot()
def openJob(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
url = self.ui.idEdit.text()
if not url:
return
for backend in self.weboob.iter_backends():
job = backend.get_job_advert(url)
if job:
self.process = QtDo(self.weboob, self.gotJob)
self.process.do('fillobj', job, backends=job.backend)
break
self.setJob(job)
self.ui.idEdit.clear()
QApplication.restoreOverrideCursor()
def gotJob(self, job):
self.setJob(job)
self.ui.queriesTabWidget.setEnabled(True)
self.process = None
def setJob(self, job):
if job:
self.ui.descriptionEdit.setText("%s" % job.description)
self.ui.titleLabel.setText("
%s
" % job.title)
self.ui.idLabel.setText("%s" % job.id)
self.ui.jobNameLabel.setText("%s" % job.job_name)
self.ui.publicationDateLabel.setText("%s" % job.publication_date)
self.ui.societyNameLabel.setText("%s" % job.society_name)
self.ui.placeLabel.setText("%s" % job.place)
self.ui.payLabel.setText("%s" % job.pay)
self.ui.contractTypeLabel.setText("%s" % job.contract_type)
self.ui.formationLabel.setText("%s" % job.formation)
self.ui.experienceLabel.setText("%s" % job.experience)
self.ui.urlLabel.setText("%s" % (job.url, job.url))
self.ui.jobFrame.show()
else:
self.ui.jobFrame.hide()
QApplication.restoreOverrideCursor()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/qhandjoob.py 0000664 0000000 0000000 00000002773 13204006733 0030560 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Sébastien Monel
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.job import CapJob
from weboob.tools.application.qt5 import QtApplication
from weboob.tools.config.yamlconfig import YamlConfig
from .main_window import MainWindow
class QHandJoob(QtApplication):
APPNAME = 'qhandjoob'
VERSION = '1.4'
COPYRIGHT = u'Copyright(C) 2013-2014 Sébastien Monel'
DESCRIPTION = "Qt application to search for job."
SHORT_DESCRIPTION = "search for job"
CAPS = CapJob
CONFIG = {'queries': {}}
STORAGE = {'bookmarks': [], 'read': [], 'notes': {}}
def main(self, argv):
self.load_backends(CapJob)
self.create_storage()
self.load_config(klass=YamlConfig)
main_window = MainWindow(self.config, self.storage, self.weboob)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026645 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030310 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030744 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhandjoob/ui/main_window.ui0000664 0000000 0000000 00000041321 13204006733 0031520 0 ustar 00root root 0000000 0000000
MainWindow00709572QHandJoobQt::Horizontal000Searchsearch00QAbstractItemView::NoEditTriggers128128true id : Advanced search00QAbstractItemView::NoEditTriggers128128trueRefreshQt::Horizontal10QFrame::StyledPanelQFrame::RaisedQt::Vertical<h1>Loading...</h1>QFormLayout::ExpandingFieldsGrow75trueIdLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse75trueJob Name75truePublication Date75trueSociety Name75truePlace75truePay75trueContract Type75trueFormation75trueExperience75trueURLLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...trueQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::VerticalQSizePolicy::Expanding205050true0070923toolBarTopToolBarAreafalseBackendssearchButtonjobList
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ 0000775 0000000 0000000 00000000000 13204006733 0026225 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/__init__.py 0000664 0000000 0000000 00000000072 13204006733 0030335 0 ustar 00root root 0000000 0000000 from .qhavedate import QHaveDate
__all__ = ['QHaveDate']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/contacts.py 0000664 0000000 0000000 00000053566 13204006733 0030434 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import time
import logging
from PyQt5.QtGui import QImage, QIcon, QPixmap
from PyQt5.QtWidgets import QWidget, QListWidgetItem, QFrame, \
QMessageBox, QTabWidget, QVBoxLayout, \
QFormLayout, QLabel, QPushButton
from PyQt5.QtCore import Qt, pyqtSlot as Slot
from weboob.tools.application.qt5 import QtDo, HTMLDelegate
from weboob.tools.application.qt5.models import BackendListModel
from weboob.tools.compat import range, basestring, unicode, long
from weboob.tools.misc import to_unicode
from weboob.capabilities.contact import CapContact, Contact
from weboob.capabilities.chat import CapChat
from weboob.capabilities.messages import CapMessages, CapMessagesPost, Message
from weboob.capabilities.base import NotLoaded
from .ui.contacts_ui import Ui_Contacts
from .ui.contact_thread_ui import Ui_ContactThread
from .ui.thread_message_ui import Ui_ThreadMessage
from .ui.profile_ui import Ui_Profile
from .ui.notes_ui import Ui_Notes
class ThreadMessage(QFrame):
"""
This class represents a message in the thread tab.
"""
def __init__(self, message, parent=None):
super(ThreadMessage, self).__init__(parent)
self.ui = Ui_ThreadMessage()
self.ui.setupUi(self)
self.set_message(message)
def set_message(self, message):
self.message = message
self.ui.nameLabel.setText(message.sender)
header = time.strftime('%Y-%m-%d %H:%M:%S', message.date.timetuple())
if message.flags & message.IS_NOT_RECEIVED:
header += u' — Unread'
elif message.flags & message.IS_RECEIVED:
header += u' — Read'
self.ui.headerLabel.setText(header)
if message.flags & message.IS_HTML:
content = message.content
else:
content = message.content.replace('&', '&').replace('<', '<').replace('>', '>').replace('\n', ' ')
self.ui.contentLabel.setText(content)
def __eq__(self, m):
if not isinstance(m, Message):
return False
return self.message == m.message
class ContactThread(QWidget):
"""
The thread of the selected contact.
"""
def __init__(self, weboob, contact, support_reply, parent=None):
super(ContactThread, self).__init__(parent)
self.ui = Ui_ContactThread()
self.ui.setupUi(self)
self.weboob = weboob
self.contact = contact
self.thread = None
self.messages = []
self.process_msg = None
self.ui.refreshButton.clicked.connect(self.refreshMessages)
if support_reply:
self.ui.sendButton.clicked.connect(self.postReply)
else:
self.ui.frame.hide()
self.refreshMessages()
@Slot()
def refreshMessages(self, fillobj=False):
if self.process_msg:
return
self.ui.refreshButton.setEnabled(False)
def finished():
#v = self.ui.scrollArea.verticalScrollBar()
#print v.minimum(), v.value(), v.maximum(), v.sliderPosition()
#self.ui.scrollArea.verticalScrollBar().setValue(self.ui.scrollArea.verticalScrollBar().maximum())
self.process_msg = None
self.process_msg = QtDo(self.weboob, self.gotThread, self.gotError, finished)
if fillobj and self.thread:
self.process_msg.do('fillobj', self.thread, ['root'], backends=self.contact.backend)
else:
self.process_msg.do('get_thread', self.contact.id, backends=self.contact.backend)
def gotError(self, backend, error, backtrace):
self.ui.textEdit.setEnabled(False)
self.ui.sendButton.setEnabled(False)
self.ui.refreshButton.setEnabled(True)
def gotThread(self, thread):
self.ui.textEdit.setEnabled(True)
self.ui.sendButton.setEnabled(True)
self.ui.refreshButton.setEnabled(True)
self.thread = thread
if thread.root is NotLoaded:
self._insert_load_button(0)
else:
for message in thread.iter_all_messages():
self._insert_message(message)
def _insert_message(self, message):
widget = ThreadMessage(message)
if widget in self.messages:
old_widget = self.messages[self.messages.index(widget)]
if old_widget.message.flags != widget.message.flags:
old_widget.set_message(widget.message)
return
for i, m in enumerate(self.messages):
if widget.message.date > m.message.date:
self.ui.scrollAreaContent.layout().insertWidget(i, widget)
self.messages.insert(i, widget)
if message.parent is NotLoaded:
self._insert_load_button(i)
return
self.ui.scrollAreaContent.layout().addWidget(widget)
self.messages.append(widget)
if message.parent is NotLoaded:
self._insert_load_button(-1)
def _insert_load_button(self, pos):
button = QPushButton(self.tr('More messages...'))
button.clicked.connect(self._load_button_pressed)
if pos >= 0:
self.ui.scrollAreaContent.layout().insertWidget(pos, button)
else:
self.ui.scrollAreaContent.layout().addWidget(button)
@Slot()
def _load_button_pressed(self):
button = self.sender()
self.ui.scrollAreaContent.layout().removeWidget(button)
button.hide()
button.deleteLater()
self.refreshMessages(fillobj=True)
@Slot()
def postReply(self):
text = self.ui.textEdit.toPlainText()
self.ui.textEdit.setEnabled(False)
self.ui.sendButton.setEnabled(False)
m = Message(thread=self.thread,
id=0,
title=u'',
sender=None,
receivers=None,
content=text,
parent=self.messages[0].message if len(self.messages) > 0 else None)
self.process_reply = QtDo(self.weboob, None, self._postReply_eb, self._postReply_fb)
self.process_reply.do('post_message', m, backends=self.contact.backend)
def _postReply_fb(self):
self.ui.textEdit.clear()
self.ui.textEdit.setEnabled(True)
self.ui.sendButton.setEnabled(True)
self.refreshMessages()
self.process_reply = None
def _postReply_eb(self, backend, error, backtrace):
content = self.tr('Unable to send message:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while posting reply'),
content, QMessageBox.Ok)
self.process_reply = None
class ContactProfile(QWidget):
def __init__(self, weboob, contact, parent=None):
super(ContactProfile, self).__init__(parent)
self.ui = Ui_Profile()
self.ui.setupUi(self)
self.ui.previousButton.clicked.connect(self.previousClicked)
self.ui.nextButton.clicked.connect(self.nextClicked)
self.weboob = weboob
self.contact = contact
self.loaded_profile = False
self.displayed_photo_idx = 0
self.process_photo = {}
missing_fields = self.gotProfile(contact)
if len(missing_fields) > 0:
self.process_contact = QtDo(self.weboob, self.gotProfile, self.gotError)
self.process_contact.do('fillobj', self.contact, missing_fields, backends=self.contact.backend)
def gotError(self, backend, error, backtrace):
self.ui.frame_photo.hide()
self.ui.descriptionEdit.setText('
' % contact.summary.replace('\n', ' '))
if not contact.profile:
missing_fields.add('profile')
elif not self.loaded_profile:
self.loaded_profile = True
for head in contact.profile.values():
if head.flags & head.HEAD:
widget = self.ui.headWidget
else:
widget = self.ui.profileTab
self.process_node(head, widget)
return missing_fields
def process_node(self, node, widget):
# Set the value widget
value = None
if node.flags & node.SECTION:
value = QWidget()
value.setLayout(QFormLayout())
for sub in node.value.values():
self.process_node(sub, value)
elif isinstance(node.value, list):
value = QLabel(' '.join(unicode(s) for s in node.value))
value.setWordWrap(True)
elif isinstance(node.value, tuple):
value = QLabel(', '.join(unicode(s) for s in node.value))
value.setWordWrap(True)
elif isinstance(node.value, (basestring,int,long,float)):
value = QLabel(unicode(node.value))
else:
logging.warning('Not supported value: %r' % node.value)
return
if isinstance(value, QLabel):
value.setTextInteractionFlags(Qt.TextSelectableByMouse|Qt.TextSelectableByKeyboard|Qt.LinksAccessibleByMouse)
# Insert the value widget into the parent widget, depending
# of its type.
if isinstance(widget, QTabWidget):
widget.addTab(value, node.label)
elif isinstance(widget.layout(), QFormLayout):
label = QLabel(u'%s: ' % node.label)
widget.layout().addRow(label, value)
elif isinstance(widget.layout(), QVBoxLayout):
widget.layout().addWidget(QLabel(u'
%s
' % node.label))
widget.layout().addWidget(value)
else:
logging.warning('Not supported widget: %r' % widget)
@Slot()
def previousClicked(self):
if len(self.contact.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx - 1) % len(self.contact.photos)
self.display_photo()
@Slot()
def nextClicked(self):
if len(self.contact.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx + 1) % len(self.contact.photos)
self.display_photo()
def display_photo(self):
if self.displayed_photo_idx >= len(self.contact.photos) or self.displayed_photo_idx < 0:
self.displayed_photo_idx = len(self.contact.photos) - 1
if self.displayed_photo_idx < 0:
self.ui.photoUrlLabel.setText('')
return
photo = list(self.contact.photos.values())[self.displayed_photo_idx]
if photo.data:
data = photo.data
if photo.id in self.process_photo:
self.process_photo.pop(photo.id)
else:
self.process_photo[photo.id] = QtDo(self.weboob, lambda p: self.display_photo())
self.process_photo[photo.id].do('fillobj', photo, ['data'], backends=self.contact.backend)
if photo.thumbnail_data:
data = photo.thumbnail_data
else:
return
img = QImage.fromData(data)
img = img.scaledToWidth(self.width()/3)
self.ui.photoLabel.setPixmap(QPixmap.fromImage(img))
if photo.url is not NotLoaded:
text = '%s' % (photo.url, photo.url)
if photo.hidden:
text += ' (Hidden photo)'
self.ui.photoUrlLabel.setText(text)
class ContactNotes(QWidget):
""" Widget for storing notes about a contact """
def __init__(self, weboob, contact, parent=None):
super(ContactNotes, self).__init__(parent)
self.ui = Ui_Notes()
self.ui.setupUi(self)
self.weboob = weboob
self.contact = contact
self.ui.textEdit.setEnabled(False)
self.ui.saveButton.setEnabled(False)
def finished():
self.process = None
self.ui.textEdit.setEnabled(True)
self.ui.saveButton.setEnabled(True)
self.process = QtDo(self.weboob, self._getNotes_cb, self._getNotes_eb, finished)
self.process.do('get_notes', self.contact.id, backends=(self.contact.backend,))
self.ui.saveButton.clicked.connect(self.saveNotes)
def _getNotes_cb(self, data):
if data:
self.ui.textEdit.setText(data)
def _getNotes_eb(self, backend, error, backtrace):
if isinstance(error, NotImplementedError):
return
self.ui.textEdit.setEnabled(True)
self.ui.saveButton.setEnabled(True)
content = self.tr('Unable to load notes:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while loading notes'),
content, QMessageBox.Ok)
@Slot()
def saveNotes(self):
text = self.ui.textEdit.toPlainText()
self.ui.saveButton.setEnabled(False)
self.ui.textEdit.setEnabled(False)
self.process = QtDo(self.weboob, None, self._saveNotes_eb, self._saveNotes_fb)
self.process.do('save_notes', self.contact.id, text, backends=(self.contact.backend,))
def _saveNotes_fb(self):
self.ui.saveButton.setEnabled(True)
self.ui.textEdit.setEnabled(True)
def _saveNotes_eb(self, backend, error, backtrace):
content = self.tr('Unable to save notes:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while saving notes'),
content, QMessageBox.Ok)
class IGroup(object):
def __init__(self, weboob, id, name):
self.id = id
self.name = name
self.weboob = weboob
def iter_contacts(self, cb):
raise NotImplementedError()
class MetaGroup(IGroup):
def iter_contacts(self, cb):
if self.id == 'online':
status = Contact.STATUS_ONLINE|Contact.STATUS_AWAY
elif self.id == 'offline':
status = Contact.STATUS_OFFLINE
else:
status = Contact.STATUS_ALL
self.process = QtDo(self.weboob, lambda d: self.cb(cb, d), fb=lambda: self.fb(cb))
self.process.do('iter_contacts', status, caps=CapContact)
def cb(self, cb, contact):
if contact:
cb(contact)
def fb(self, fb):
self.process = None
if fb:
fb(None)
class ContactsWidget(QWidget):
def __init__(self, weboob, parent=None):
super(ContactsWidget, self).__init__(parent)
self.ui = Ui_Contacts()
self.ui.setupUi(self)
self.weboob = weboob
self.contact = None
self.ui.contactList.setItemDelegate(HTMLDelegate())
self.url_process = None
self.photo_processes = {}
self.ui.groupBox.addItem('All', MetaGroup(self.weboob, 'all', self.tr('All')))
self.ui.groupBox.addItem('Online', MetaGroup(self.weboob, 'online', self.tr('Online')))
self.ui.groupBox.addItem('Offline', MetaGroup(self.weboob, 'offline', self.tr('Offline')))
self.ui.groupBox.setCurrentIndex(1)
self.ui.groupBox.currentIndexChanged.connect(self.groupChanged)
self.ui.contactList.itemClicked.connect(self.contactChanged)
self.ui.refreshButton.clicked.connect(self.refreshContactList)
self.ui.urlButton.clicked.connect(self.urlClicked)
def load(self):
self.refreshContactList()
model = BackendListModel(self.weboob)
model.addBackends(entry_all=False)
self.ui.backendsList.setModel(model)
@Slot()
def groupChanged(self):
self.refreshContactList()
@Slot()
def refreshContactList(self):
self.ui.contactList.clear()
self.ui.refreshButton.setEnabled(False)
i = self.ui.groupBox.currentIndex()
group = self.ui.groupBox.itemData(i)
group.iter_contacts(self.addContact)
def setPhoto(self, contact, item):
if not contact:
return False
try:
self.photo_processes.pop(contact.id, None)
except KeyError:
pass
img = None
for photo in contact.photos.values():
if photo.thumbnail_data:
img = QImage.fromData(photo.thumbnail_data)
break
if img:
item.setIcon(QIcon(QPixmap.fromImage(img)))
return True
return False
def addContact(self, contact):
if not contact:
self.ui.refreshButton.setEnabled(True)
return
status = ''
if contact.status == Contact.STATUS_ONLINE:
status = u'Online'
status_color = 0x00aa00
elif contact.status == Contact.STATUS_OFFLINE:
status = u'Offline'
status_color = 0xff0000
elif contact.status == Contact.STATUS_AWAY:
status = u'Away'
status_color = 0xffad16
else:
status = u'Unknown'
status_color = 0xaaaaaa
if contact.status_msg:
status += u' — %s' % contact.status_msg
item = QListWidgetItem()
item.setText('
%s
%s %s' % (contact.name, status_color, status, contact.backend))
item.setData(Qt.UserRole, contact)
if contact.photos is NotLoaded:
process = QtDo(self.weboob, lambda c: self.setPhoto(c, item))
process.do('fillobj', contact, ['photos'], backends=contact.backend)
self.photo_processes[contact.id] = process
elif len(contact.photos) > 0:
if not self.setPhoto(contact, item):
photo = list(contact.photos.values())[0]
process = QtDo(self.weboob, lambda p: self.setPhoto(contact, item))
process.do('fillobj', photo, ['thumbnail_data'], backends=contact.backend)
self.photo_processes[contact.id] = process
for i in range(self.ui.contactList.count()):
if self.ui.contactList.item(i).data(Qt.UserRole).status > contact.status:
self.ui.contactList.insertItem(i, item)
return
self.ui.contactList.addItem(item)
@Slot(QListWidgetItem)
def contactChanged(self, current):
if not current:
return
contact = current.data(Qt.UserRole)
self.setContact(contact)
def setContact(self, contact):
if not contact or contact == self.contact:
return
if not isinstance(contact, Contact):
return self.retrieveContact(contact)
self.ui.tabWidget.clear()
self.contact = contact
backend = self.weboob.get_backend(self.contact.backend)
self.ui.tabWidget.addTab(ContactProfile(self.weboob, self.contact), self.tr('Profile'))
if backend.has_caps(CapMessages):
self.ui.tabWidget.addTab(ContactThread(self.weboob, self.contact, backend.has_caps(CapMessagesPost)), self.tr('Messages'))
if backend.has_caps(CapChat):
self.ui.tabWidget.setTabEnabled(self.ui.tabWidget.addTab(QWidget(), self.tr('Chat')),
False)
self.ui.tabWidget.setTabEnabled(self.ui.tabWidget.addTab(QWidget(), self.tr('Calendar')),
False)
self.ui.tabWidget.addTab(ContactNotes(self.weboob, self.contact), self.tr('Notes'))
@Slot()
def urlClicked(self):
url = self.ui.urlEdit.text()
if not url:
return
self.retrieveContact(url)
def retrieveContact(self, url):
backend_name = self.ui.backendsList.currentText()
self.ui.urlButton.setEnabled(False)
def finished():
self.url_process = None
self.ui.urlButton.setEnabled(True)
self.url_process = QtDo(self.weboob, self.retrieveContact_cb, self.retrieveContact_eb, finished)
self.url_process.do('get_contact', url, backends=backend_name)
def retrieveContact_cb(self, contact):
self.ui.urlEdit.clear()
self.setContact(contact)
def retrieveContact_eb(self, backend, error, backtrace):
content = self.tr('Unable to get contact:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += u'\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while getting contact'),
content, QMessageBox.Ok)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/events.py 0000664 0000000 0000000 00000013612 13204006733 0030106 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from PyQt5.QtGui import QImage, QIcon, QPixmap
from PyQt5.QtWidgets import QWidget, QTreeWidgetItem
from PyQt5.QtCore import Qt, pyqtSignal as Signal, pyqtSlot as Slot
from weboob.capabilities.base import NotLoaded
from weboob.tools.compat import range
from weboob.tools.application.qt5 import QtDo, HTMLDelegate
from .ui.events_ui import Ui_Events
class EventsWidget(QWidget):
display_contact = Signal(object)
def __init__(self, weboob, parent=None):
super(EventsWidget, self).__init__(parent)
self.ui = Ui_Events()
self.ui.setupUi(self)
self.weboob = weboob
self.photo_processes = {}
self.event_filter = None
self.ui.eventsList.itemDoubleClicked.connect(self.eventDoubleClicked)
self.ui.typeBox.currentIndexChanged.connect(self.typeChanged)
self.ui.refreshButton.clicked.connect(self.refreshEventsList)
self.ui.eventsList.setItemDelegate(HTMLDelegate())
self.ui.eventsList.sortByColumn(1, Qt.DescendingOrder)
def load(self):
self.refreshEventsList()
@Slot(int)
def typeChanged(self, i):
if self.ui.refreshButton.isEnabled():
self.refreshEventsList()
@Slot()
def refreshEventsList(self):
self.ui.eventsList.clear()
self.ui.refreshButton.setEnabled(False)
if self.ui.typeBox.currentIndex() >= 0:
# XXX strangely, in gotEvent() in the loop to check if there is already the
# event type to try to introduce it in list, itemData() returns the right value.
# But, I don't know why, here, it will ALWAYS return None...
# So the filter does not work currently.
self.events_filter = self.ui.typeBox.itemData(self.ui.typeBox.currentIndex())
else:
self.event_filter = None
self.ui.typeBox.setEnabled(False)
self.ui.typeBox.clear()
self.ui.typeBox.addItem('All', None)
def finished():
self.ui.refreshButton.setEnabled(True)
self.ui.typeBox.setEnabled(True)
self.process = QtDo(self.weboob, self.gotEvent, fb=finished)
self.process.do('iter_events')
def setPhoto(self, contact, item):
if not contact:
return False
try:
self.photo_processes.pop(contact.id, None)
except KeyError:
pass
img = None
for photo in contact.photos.values():
if photo.thumbnail_data:
img = QImage.fromData(photo.thumbnail_data)
break
if img:
item.setIcon(0, QIcon(QPixmap.fromImage(img)))
self.ui.eventsList.resizeColumnToContents(0)
return True
return False
def gotEvent(self, event):
found = False
for i in range(self.ui.typeBox.count()):
s = self.ui.typeBox.itemData(i)
if s == event.type:
found = True
if not found:
print(event.type)
self.ui.typeBox.addItem(event.type.capitalize(), event.type)
if event.type == self.event_filter:
self.ui.typeBox.setCurrentIndex(self.ui.typeBox.count()-1)
if self.event_filter and self.event_filter != event.type:
return
if not event.contact:
return
contact = event.contact
contact.backend = event.backend
status = ''
if contact.status == contact.STATUS_ONLINE:
status = u'Online'
status_color = 0x00aa00
elif contact.status == contact.STATUS_OFFLINE:
status = u'Offline'
status_color = 0xff0000
elif contact.status == contact.STATUS_AWAY:
status = u'Away'
status_color = 0xffad16
else:
status = u'Unknown'
status_color = 0xaaaaaa
if contact.status_msg:
status += u' — %s' % contact.status_msg
name = '
%s
%s %s' % (contact.name, status_color, status, event.backend)
date = event.date.strftime('%Y-%m-%d %H:%M')
type = event.type
message = event.message
item = QTreeWidgetItem(None, [name, date, type, message])
item.setData(0, Qt.UserRole, event)
if contact.photos is NotLoaded:
process = QtDo(self.weboob, lambda c: self.setPhoto(c, item))
process.do('fillobj', contact, ['photos'], backends=contact.backend)
self.photo_processes[contact.id] = process
elif len(contact.photos) > 0:
if not self.setPhoto(contact, item):
photo = list(contact.photos.values())[0]
process = QtDo(self.weboob, lambda p: self.setPhoto(contact, item))
process.do('fillobj', photo, ['thumbnail_data'], backends=contact.backend)
self.photo_processes[contact.id] = process
self.ui.eventsList.addTopLevelItem(item)
self.ui.eventsList.resizeColumnToContents(0)
self.ui.eventsList.resizeColumnToContents(1)
@Slot(QTreeWidgetItem, int)
def eventDoubleClicked(self, item, col):
event = item.data(0, Qt.UserRole)
self.display_contact.emit(event.contact)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/main_window.py 0000664 0000000 0000000 00000006434 13204006733 0031121 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSlot as Slot
from weboob.tools.application.qt5 import QtMainWindow
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.capabilities.dating import CapDating
try:
from weboob.applications.qboobmsg.messages_manager import MessagesManager
HAVE_BOOBMSG = True
except ImportError:
HAVE_BOOBMSG = False
from .ui.main_window_ui import Ui_MainWindow
from .status import AccountsStatus
from .contacts import ContactsWidget
from .events import EventsWidget
from .search import SearchWidget
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.loaded_tabs = {}
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.ui.tabWidget.currentChanged.connect(self.tabChanged)
self.addTab(AccountsStatus(self.weboob), self.tr('Status'))
self.addTab(MessagesManager(self.weboob) if HAVE_BOOBMSG else None, self.tr('Messages'))
self.addTab(ContactsWidget(self.weboob), self.tr('Contacts'))
self.addTab(EventsWidget(self.weboob), self.tr('Events'))
self.addTab(SearchWidget(self.weboob), self.tr('Search'))
self.addTab(None, self.tr('Calendar'))
self.addTab(None, self.tr('Optimizations'))
if self.weboob.count_backends() == 0:
self.backendsConfig()
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapDating,), self)
if bckndcfg.run():
self.loaded_tabs.clear()
widget = self.ui.tabWidget.widget(self.ui.tabWidget.currentIndex())
widget.load()
def addTab(self, widget, title):
if widget:
if getattr(widget, 'display_contact', None):
widget.display_contact.connect(self.display_contact)
self.ui.tabWidget.addTab(widget, title)
else:
index = self.ui.tabWidget.addTab(QWidget(), title)
self.ui.tabWidget.setTabEnabled(index, False)
@Slot(int)
def tabChanged(self, i):
widget = self.ui.tabWidget.currentWidget()
if hasattr(widget, 'load') and i not in self.loaded_tabs:
widget.load()
self.loaded_tabs[i] = True
@Slot(object)
def display_contact(self, contact):
self.ui.tabWidget.setCurrentIndex(2)
widget = self.ui.tabWidget.currentWidget()
widget.setContact(contact)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/qhavedate.py 0000664 0000000 0000000 00000002654 13204006733 0030550 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.dating import CapDating
from weboob.tools.application.qt5 import QtApplication
from .main_window import MainWindow
class QHaveDate(QtApplication):
APPNAME = 'qhavedate'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-2014 Romain Bignon'
DESCRIPTION = "Qt application allowing to interact with various dating websites."
SHORT_DESCRIPTION = "interact with dating websites"
CAPS = CapDating
STORAGE_FILENAME = 'dating.storage'
def main(self, argv):
self.create_storage(self.STORAGE_FILENAME)
self.load_backends(CapDating)
main_window = MainWindow(self.config, self.weboob)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/search.py 0000664 0000000 0000000 00000006276 13204006733 0030057 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSlot as Slot
from weboob.tools.application.qt5 import QtDo
from .ui.search_ui import Ui_Search
from .contacts import ContactProfile
from .status import Account
class SearchWidget(QWidget):
def __init__(self, weboob, parent=None):
super(SearchWidget, self).__init__(parent)
self.ui = Ui_Search()
self.ui.setupUi(self)
self.weboob = weboob
self.contacts = []
self.accounts = []
self.current = None
self.ui.nextButton.clicked.connect(self.next)
self.ui.queryButton.clicked.connect(self.sendQuery)
def load(self):
while self.ui.statusFrame.layout().count() > 0:
item = self.ui.statusFrame.layout().takeAt(0)
if item.widget():
item.widget().deinit()
item.widget().hide()
item.widget().deleteLater()
self.accounts = []
for backend in self.weboob.iter_backends():
account = Account(self.weboob, backend)
account.title.setText(u'
%s
' % backend.name)
self.accounts.append(account)
self.ui.statusFrame.layout().addWidget(account)
self.ui.statusFrame.layout().addStretch()
self.getNewProfiles()
def updateStats(self):
for account in self.accounts:
account.updateStats()
def getNewProfiles(self):
self.newprofiles_process = QtDo(self.weboob, self.retrieveNewContacts_cb)
self.newprofiles_process.do('iter_new_contacts')
def retrieveNewContacts_cb(self, contact):
self.contacts.insert(0, contact)
self.ui.queueLabel.setText('%d' % len(self.contacts))
if self.current is None:
next(self)
@Slot()
def next(self):
try:
contact = self.contacts.pop()
except IndexError:
contact = None
self.ui.queueLabel.setText('%d' % len(self.contacts))
self.setContact(contact)
self.updateStats()
def setContact(self, contact):
self.current = contact
if contact is not None:
widget = ContactProfile(self.weboob, contact)
self.ui.scrollArea.setWidget(widget)
else:
self.ui.scrollArea.setWidget(None)
@Slot()
def sendQuery(self):
self.newprofiles_process = QtDo(self.weboob, None, fb=self.next)
self.newprofiles_process.do('send_query', self.current.id, backends=[self.current.backend])
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/status.py 0000664 0000000 0000000 00000010767 13204006733 0030135 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QScrollArea, QWidget, QHBoxLayout, QVBoxLayout, QFrame, QLabel
from weboob.capabilities.account import CapAccount, StatusField
from weboob.tools.application.qt5 import QtDo
from weboob.tools.misc import to_unicode
class Account(QFrame):
def __init__(self, weboob, backend, parent=None):
super(Account, self).__init__(parent)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.weboob = weboob
self.backend = backend
self.setLayout(QVBoxLayout())
self.timer = None
head = QHBoxLayout()
headw = QWidget()
headw.setLayout(head)
self.title = QLabel(u'
"
self.body.setText(self.process.body)
self.process = None
def updateStats_cb(self, field):
if field.flags & StatusField.FIELD_HTML:
value = u'%s' % field.value
else:
value = (u'%s' % field.value).replace('&', '&').replace('<', '<').replace('>', '>')
if field.flags & StatusField.FIELD_TEXT:
if self.process.in_p:
self.process.body += u''
self.process.body += u'
%s
' % value
self.process.in_p = False
else:
if not self.process.in_p:
self.process.body += u"
"
self.process.in_p = True
else:
self.process.body += u" "
self.process.body += u'%s: %s' % (field.label, field.value)
def updateStats_eb(self, backend, err, backtrace):
self.body.setText(u'Unable to connect: %s' % to_unicode(err))
self.title.setText(u'%s' % self.title.text())
class AccountsStatus(QScrollArea):
def __init__(self, weboob, parent=None):
super(AccountsStatus, self).__init__(parent)
self.weboob = weboob
self.setFrameShadow(self.Plain)
self.setFrameShape(self.NoFrame)
self.setWidgetResizable(True)
widget = QWidget(self)
widget.setLayout(QVBoxLayout())
widget.show()
self.setWidget(widget)
def load(self):
while self.widget().layout().count() > 0:
item = self.widget().layout().takeAt(0)
if item.widget():
item.widget().deinit()
item.widget().hide()
item.widget().deleteLater()
for backend in self.weboob.iter_backends():
account = Account(self.weboob, backend)
self.widget().layout().addWidget(account)
self.widget().layout().addStretch()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026642 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030305 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030741 0 ustar 00root root 0000000 0000000 contact_thread.ui 0000664 0000000 0000000 00000011122 13204006733 0032101 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui
ContactThread00578429FormQt::Vertical00QFrame::StyledPanelQFrame::Raisedfalsefalse00Send050Qt::Horizontal4020../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png01Qt::ScrollBarAsNeededtrueQt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft00556154QWidget#scrollAreaContent {
background-color: rgb(255, 255, 255);
}
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/contacts.ui 0000664 0000000 0000000 00000007307 13204006733 0031026 0 ustar 00root root 0000000 0000000
Contacts00478374FormQt::Horizontal00QFrame::StyledPanelQFrame::Raised../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png1201201truefalsetrueFrom URLDisplay10
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/events.ui 0000664 0000000 0000000 00000004525 13204006733 0030513 0 ustar 00root root 0000000 0000000
Events00497318Form../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.pngQAbstractItemView::NoEditTriggerstrue6464falsetruetrueContactDateTypeMessage
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/main_window.ui0000664 0000000 0000000 00000004170 13204006733 0031516 0 ustar 00root root 0000000 0000000
MainWindow00763580QHaveDate-10076324FiletoolBarTopToolBarAreafalseBackendsQuitQuitactionQuittriggered()MainWindowclose()-1-1381289
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/notes.ui 0000664 0000000 0000000 00000001235 13204006733 0030332 0 ustar 00root root 0000000 0000000
Notes00430323FormSave
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/profile.ui 0000664 0000000 0000000 00000021211 13204006733 0030636 0 ustar 00root root 0000000 0000000
Profile00755647Form0QFrame::NoFrameQFrame::Plaintrue0075564700000QFrame::StyledPanelQFrame::Raised<h1>Loading...</h1>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse<b>URL:</b>trueQt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse0900Qt::Vertical200QFrame::StyledPanelQFrame::Raised002016777215<Qt::AlignCenter002016777215>Qt::AlignCenterQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::Vertical130Qt::Horizontal00true20-1
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui/search.ui 0000664 0000000 0000000 00000006303 13204006733 0030450 0 ustar 00root root 0000000 0000000
Search00438349Form4030016777215QFrame::StyledPanelQFrame::Raised<b>Profiles in queue:</b>Qt::Horizontal4020QFrame::StyledPanelQFrame::RaisedQueryNexttrue00178235
thread_message.ui 0000664 0000000 0000000 00000005370 13204006733 0032102 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qhavedate/ui
ThreadMessage0055276FrameQFrame::StyledPanelQFrame::RaisedQt::Vertical20100trueQt::LinksAccessibleByMouse|Qt::TextSelectableByMousetrueQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::Vertical201
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ 0000775 0000000 0000000 00000000000 13204006733 0026073 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/__init__.py 0000664 0000000 0000000 00000000067 13204006733 0030207 0 ustar 00root root 0000000 0000000 from .qvideoob import QVideoob
__all__ = ['QVideoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/main_window.py 0000664 0000000 0000000 00000012374 13204006733 0030767 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtCore import pyqtSlot as Slot
from weboob.capabilities.video import CapVideo
from weboob.tools.application.qt5 import QtMainWindow, QtDo
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.models import BackendListModel
from weboob.applications.qvideoob.ui.main_window_ui import Ui_MainWindow
from .video import Video
from .minivideo import MiniVideo
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.minivideos = []
self.app = app
self.ui.sortbyEdit.setCurrentIndex(int(self.config.get('settings', 'sortby')))
self.ui.nsfwCheckBox.setChecked(int(self.config.get('settings', 'nsfw')))
self.ui.sfwCheckBox.setChecked(int(self.config.get('settings', 'sfw')))
self.ui.searchEdit.returnPressed.connect(self.search)
self.ui.urlEdit.returnPressed.connect(self.openURL)
self.ui.nsfwCheckBox.stateChanged.connect(self.nsfwChanged)
self.ui.sfwCheckBox.stateChanged.connect(self.sfwChanged)
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
@Slot()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapVideo,), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
model = BackendListModel(self.weboob)
model.addBackends()
self.ui.backendEdit.setModel(model)
current_backend = self.config.get('settings', 'backend')
idx = self.ui.backendEdit.findData(current_backend)
if idx >= 0:
self.ui.backendEdit.setCurrentIndex(idx)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
self.ui.urlEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
self.ui.urlEdit.setEnabled(True)
@Slot(int)
def nsfwChanged(self, state):
self.config.set('settings', 'nsfw', int(self.ui.nsfwCheckBox.isChecked()))
self.updateVideosDisplay()
@Slot(int)
def sfwChanged(self, state):
self.config.set('settings', 'sfw', int(self.ui.sfwCheckBox.isChecked()))
self.updateVideosDisplay()
def updateVideosDisplay(self):
for minivideo in self.minivideos:
if (minivideo.video.nsfw and self.ui.nsfwCheckBox.isChecked() or
not minivideo.video.nsfw and self.ui.sfwCheckBox.isChecked()):
minivideo.show()
else:
minivideo.hide()
@Slot()
def search(self):
pattern = self.ui.searchEdit.text()
if not pattern:
return
for minivideo in self.minivideos:
self.ui.scrollAreaContent.layout().removeWidget(minivideo)
minivideo.hide()
minivideo.deleteLater()
self.minivideos = []
self.ui.searchEdit.setEnabled(False)
backend_name = self.ui.backendEdit.itemData(self.ui.backendEdit.currentIndex())
def finished():
self.ui.searchEdit.setEnabled(True)
self.process = None
self.process = QtDo(self.weboob, self.addVideo, fb=finished)
self.process.do(self.app._do_complete, 20, (), 'search_videos', pattern, self.ui.sortbyEdit.currentIndex(), nsfw=True, backends=backend_name)
def addVideo(self, video):
minivideo = MiniVideo(self.weboob, self.weboob[video.backend], video)
self.ui.scrollAreaContent.layout().addWidget(minivideo)
self.minivideos.append(minivideo)
if (video.nsfw and not self.ui.nsfwCheckBox.isChecked() or
not video.nsfw and not self.ui.sfwCheckBox.isChecked()):
minivideo.hide()
@Slot()
def openURL(self):
url = self.ui.urlEdit.text()
if not url:
return
for backend in self.weboob.iter_backends():
video = backend.get_video(url)
if video:
video_widget = Video(video, self)
video_widget.show()
self.ui.urlEdit.clear()
def closeEvent(self, ev):
self.config.set('settings', 'backend', self.ui.backendEdit.itemData(self.ui.backendEdit.currentIndex()))
self.config.set('settings', 'sortby', self.ui.sortbyEdit.currentIndex())
self.config.save()
ev.accept()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/minivideo.py 0000664 0000000 0000000 00000005043 13204006733 0030432 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFrame
from weboob.tools.application.qt5 import QtDo
from weboob.tools.compat import unicode
from weboob.applications.qvideoob.ui.minivideo_ui import Ui_MiniVideo
from .video import Video
class MiniVideo(QFrame):
def __init__(self, weboob, backend, video, parent=None):
super(MiniVideo, self).__init__(parent)
self.ui = Ui_MiniVideo()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.video = video
self.ui.titleLabel.setText(video.title)
self.ui.backendLabel.setText(backend.name)
self.ui.durationLabel.setText(unicode(video.duration))
self.ui.authorLabel.setText(unicode(video.author))
self.ui.dateLabel.setText(video.date and unicode(video.date) or '')
if video.rating_max:
self.ui.ratingLabel.setText('%s / %s' % (video.rating, video.rating_max))
else:
self.ui.ratingLabel.setText('%s' % video.rating)
self.process_thumbnail = QtDo(self.weboob, self.gotThumbnail)
self.process_thumbnail.do('fillobj', self.video, ['thumbnail'], backends=backend)
def gotThumbnail(self, video):
if video.thumbnail and video.thumbnail.data:
img = QImage.fromData(video.thumbnail.data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img))
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
video = self.backend.fillobj(self.video)
if video:
video_widget = Video(video, self)
video_widget.show()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/qvideoob.py 0000664 0000000 0000000 00000003066 13204006733 0030262 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.video import CapVideo
from weboob.tools.application.qt5 import QtApplication
from .main_window import MainWindow
class QVideoob(QtApplication):
APPNAME = 'qvideoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-2014 Romain Bignon'
DESCRIPTION = "Qt application allowing to search videos on various websites and play them."
SHORT_DESCRIPTION = "search and play videos"
CAPS = CapVideo
CONFIG = {'settings': {'nsfw': 1,
'sfw': 1,
'sortby': 0,
'backend': ''
}
}
def main(self, argv):
self.load_backends(CapVideo)
self.load_config()
main_window = MainWindow(self.config, self.weboob, self)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ui/ 0000775 0000000 0000000 00000000000 13204006733 0026510 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ui/Makefile 0000664 0000000 0000000 00000000265 13204006733 0030153 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ui/__init__.py 0000664 0000000 0000000 00000000000 13204006733 0030607 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ui/main_window.ui 0000664 0000000 0000000 00000012411 13204006733 0031361 0 ustar 00root root 0000000 0000000
MainWindow00582463QVideoobQFrame::StyledPanelQFrame::RaisedSearch: 00RelevanceRatingDurationDate10000Display:SFWtrueNSFWtrueQt::Horizontal4020true00560230QFrame::StyledPanelQFrame::RaisedURL: 0058225toolBarTopToolBarAreafalseBackends
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ui/minivideo.ui 0000664 0000000 0000000 00000011340 13204006733 0031031 0 ustar 00root root 0000000 0000000
MiniVideo00464132FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueTitle0050truefalseTextLabel75trueDurationTextLabel75trueAuthorTextLabel75trueDateTextLabel75trueRatingTextLabel75trueWhereTextLabel
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/ui/video.ui 0000664 0000000 0000000 00000013175 13204006733 0030164 0 ustar 00root root 0000000 0000000
Video00647404Video1275trueQFrame::BoxQFrame::RaisedTextLabelQt::AlignCenter00QFrame::StyledPanelQFrame::Sunken00Qt::HorizontalQFrame::StyledPanelQFrame::RaisedQFormLayout::ExpandingFieldsGrow75trueURLtrueArrowCursortruefalsetrue75trueDurationTextLabel75trueAuthorTextLabel75trueDateTextLabel75trueRatingTextLabelQVideoWidgetQWidgetPyQt5.QtMultimediaWidgets1
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qvideoob/video.py 0000664 0000000 0000000 00000005161 13204006733 0027556 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt5.QtCore import QUrl, pyqtSlot as Slot
from PyQt5.QtWidgets import QDialog
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from weboob.applications.qvideoob.ui.video_ui import Ui_Video
from weboob.tools.compat import unicode
class Video(QDialog):
def __init__(self, video, parent=None):
super(Video, self).__init__(parent)
self.ui = Ui_Video()
self.ui.setupUi(self)
self.video = video
self.setWindowTitle("Video - %s" % video.title)
self.ui.urlEdit.setText(video.url)
self.ui.titleLabel.setText(video.title)
self.ui.durationLabel.setText(unicode(video.duration))
self.ui.authorLabel.setText(unicode(video.author))
self.ui.dateLabel.setText(unicode(video.date))
if video.rating_max:
self.ui.ratingLabel.setText('%s / %s' % (video.rating, video.rating_max))
else:
self.ui.ratingLabel.setText('%s' % video.rating)
self.mediaPlayer = QMediaPlayer()
self.mediaPlayer.durationChanged.connect(self._setMax)
self.mediaPlayer.seekableChanged.connect(self.ui.seekSlider.setEnabled)
self.mediaPlayer.positionChanged.connect(self._slide)
self.ui.seekSlider.valueChanged.connect(self.mediaPlayer.setPosition)
mc = QMediaContent(QUrl(video.url))
self.mediaPlayer.setMedia(mc)
self.ui.videoPlayer.setMediaObject(self.mediaPlayer)
self.mediaPlayer.play()
@Slot('qint64')
def _slide(self, pos):
blocking = self.ui.seekSlider.blockSignals(True)
self.ui.seekSlider.setValue(pos)
self.ui.seekSlider.blockSignals(blocking)
@Slot('qint64')
def _setMax(self, duration):
self.ui.seekSlider.setMaximum(duration)
def closeEvent(self, event):
self.mediaPlayer.stop()
event.accept()
def hideEvent(self, event):
self.mediaPlayer.stop()
event.accept()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit/ 0000775 0000000 0000000 00000000000 13204006733 0027462 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit/__init__.py0000664 0000000 0000000 00000000115 13204006733 0031570 0 ustar 00root root 0000000 0000000 from .qwebcontentedit import QWebContentEdit
__all__ = ['QWebContentEdit']
main_window.py 0000664 0000000 0000000 00000024247 13204006733 0032301 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit # -*- coding: utf-8 -*-
# Copyright(C) 2011 Clément Schreiner
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import logging
from copy import deepcopy
from PyQt5.QtCore import pyqtSlot as Slot
from PyQt5.QtWidgets import QMessageBox, QTableWidgetItem
from PyQt5.QtCore import Qt
from weboob.tools.application.base import MoreResultsAvailable
from weboob.tools.application.qt5 import QtMainWindow, QtDo
from weboob.tools.application.qt5.backendcfg import BackendCfg
from weboob.tools.application.qt5.models import BackendListModel
from weboob.capabilities.content import CapContent
from weboob.tools.misc import to_unicode
from .ui.main_window_ui import Ui_MainWindow
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.backend = None
self.app = app
self.ui.idEdit.returnPressed.connect(self.loadPage)
self.ui.loadButton.clicked.connect(self.loadPage)
self.ui.tabWidget.currentChanged.connect(self._currentTabChanged)
self.ui.saveButton.clicked.connect(self.savePage)
self.ui.actionBackends.triggered.connect(self.backendsConfig)
self.ui.contentEdit.textChanged.connect(self._textChanged)
self.ui.loadHistoryButton.clicked.connect(self.loadHistory)
if hasattr(self.ui.descriptionEdit, "setPlaceholderText"):
self.ui.descriptionEdit.setPlaceholderText("Edit summary")
if self.weboob.count_backends() == 0:
self.backendsConfig()
else:
self.loadBackends()
@Slot()
def backendsConfig(self):
""" Opens backends configuration dialog when 'Backends' is clicked """
bckndcfg = BackendCfg(self.weboob, (CapContent,), self)
if bckndcfg.run():
self.loadBackends()
def loadBackends(self):
""" Fills the backends comboBox with available backends """
model = BackendListModel(self.weboob)
model.addBackends(entry_all=False)
self.ui.backendBox.setModel(model)
@Slot()
def _currentTabChanged(self):
""" Loads history or preview when the corresponding tabs are shown """
if self.ui.tabWidget.currentIndex() == 1:
if self.backend is not None:
self.loadPreview()
elif self.ui.tabWidget.currentIndex() == 2:
if self.backend is not None:
self.loadHistory()
@Slot()
def _textChanged(self):
""" The text in the content QPlainTextEdit has changed """
if self.backend:
self.ui.saveButton.setEnabled(True)
self.ui.saveButton.setText('Save')
@Slot()
def loadPage(self):
""" Loads a page's source into the 'content' QPlainTextEdit """
_id = self.ui.idEdit.text()
if not _id:
return
self.ui.loadButton.setEnabled(False)
self.ui.loadButton.setText('Loading...')
self.ui.contentEdit.setReadOnly(True)
backend = self.ui.backendBox.currentText()
def finished():
self.process = None
if self.backend:
self.ui.contentEdit.setReadOnly(False)
self.ui.loadButton.setEnabled(True)
self.ui.loadButton.setText('Load')
self.process = QtDo(self.weboob,
self._loadedPage,
self._errorLoadPage,
finished)
self.process.do('get_content', _id, backends=(backend,))
def _loadedPage(self, data):
""" Callback for loadPage """
if not data:
self.content = None
self.backend = None
QMessageBox.critical(self, self.tr('Unable to open page'),
'Unable to open page "%s" on %s: it does not exist.'
% (self.ui.idEdit.text(),
self.ui.backendBox.currentText()),
QMessageBox.Ok)
return
self.content = data
self.ui.contentEdit.setPlainText(self.content.content)
self.setWindowTitle("QWebcontentedit - %s@%s" %(self.content.id,
self.content.backend))
self.backend = self.weboob[self.content.backend]
def _errorLoadPage(self, backend, error, backtrace):
""" Error callback for loadPage """
content = self.tr('Unable to load page:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while loading page'),
content, QMessageBox.Ok)
self.ui.loadButton.setEnabled(True)
self.ui.loadButton.setText("Load")
@Slot()
def savePage(self):
""" Saves the current page to the remote site """
if self.backend is None:
return
new_content = self.ui.contentEdit.toPlainText()
minor = self.ui.minorBox.isChecked()
if new_content != self.content.content:
self.ui.saveButton.setEnabled(False)
self.ui.saveButton.setText('Saving...')
self.ui.contentEdit.setReadOnly(True)
self.content.content = new_content
message = self.ui.descriptionEdit.text()
def finished():
self.process = None
self.ui.saveButton.setText('Saved')
self.ui.descriptionEdit.clear()
self.ui.contentEdit.setReadOnly(False)
self.process = QtDo(self.weboob,
None,
self._errorSavePage,
finished)
self.process.do('push_content',
self.content,
message,
minor=minor,
backends=self.backend)
def _errorSavePage(self, backend, error, backtrace):
""" """
content = self.tr('Unable to save page:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while saving page'),
content, QMessageBox.Ok)
self.ui.saveButton.setEnabled(True)
self.ui.saveButton.setText("Save")
def loadPreview(self):
""" Loads the current page's preview into the preview QTextEdit """
tmp_content = deepcopy(self.content)
tmp_content.content = self.ui.contentEdit.toPlainText()
self.ui.previewEdit.setHtml(self.backend.get_content_preview(tmp_content))
@Slot()
def loadHistory(self):
""" Loads the page's log into the 'History' tab """
if self.backend is None:
return
self.ui.loadHistoryButton.setEnabled(False)
self.ui.loadHistoryButton.setText("Loading...")
self.ui.historyTable.clear()
self.ui.historyTable.setRowCount(0)
self.ui.historyTable.setHorizontalHeaderLabels(["Revision",
"Time",
"Author",
"Summary"])
self.ui.historyTable.setColumnWidth(3, 1000)
def finished():
self.process = None
self.ui.loadHistoryButton.setEnabled(True)
self.ui.loadHistoryButton.setText("Reload")
self.process = QtDo(self.weboob,
self._gotRevision,
self._errorHistory,
finished)
self.process.do(self.app._do_complete,
self.ui.nbRevBox.value(),
(),
'iter_revisions',
self.content.id,
backends=(self.backend,))
def _gotRevision(self, revision):
""" Callback for loadHistory's QtDo """
# we set the flags to Qt.ItemIsEnabled so that the items
# are not modifiable (they are modifiable by default)
item_revision = QTableWidgetItem(revision.id)
item_revision.setFlags(Qt.ItemIsEnabled)
item_time = QTableWidgetItem(revision.timestamp.strftime('%Y-%m-%d %H:%M:%S'))
item_time.setFlags(Qt.ItemIsEnabled)
item_author = QTableWidgetItem(revision.author)
item_author.setFlags(Qt.ItemIsEnabled)
item_summary = QTableWidgetItem(revision.comment)
item_summary.setFlags(Qt.ItemIsEnabled)
row = self.ui.historyTable.currentRow() + 1
self.ui.historyTable.insertRow(row)
self.ui.historyTable.setItem(row, 0, item_revision)
self.ui.historyTable.setItem(row, 1, item_time)
self.ui.historyTable.setItem(row, 2, item_author)
self.ui.historyTable.setItem(row, 3, item_summary)
self.ui.historyTable.setCurrentCell(row, 0)
def _errorHistory(self, backend, error, backtrace):
""" Loading the history has failed """
if isinstance(error, MoreResultsAvailable):
return
content = self.tr('Unable to load history:\n%s\n') % to_unicode(error)
if logging.root.level <= logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while loading history'),
content, QMessageBox.Ok)
self.ui.loadHistoryButton.setEnabled(True)
self.ui.loadHistoryButton.setText("Reload")
qwebcontentedit.py 0000664 0000000 0000000 00000002602 13204006733 0033154 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit # -*- coding: utf-8 -*-
# Copyright(C) 2011 Clément Schreiner
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.application.qt5 import QtApplication
from weboob.capabilities.content import CapContent
from .main_window import MainWindow
class QWebContentEdit(QtApplication):
APPNAME = 'qwebcontentedit'
VERSION = '1.4'
COPYRIGHT = u'Copyright(C) 2011-2014 Clément Schreiner'
DESCRIPTION = "Qt application allowing to manage content of various websites."
SHORT_DESCRIPTION = "manage websites content"
CAPS = CapContent
def main(self, argv):
self.load_backends(CapContent, storage=self.create_storage())
main_window = MainWindow(self.config, self.weboob, self)
main_window.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit/ui/ 0000775 0000000 0000000 00000000000 13204006733 0030077 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit/ui/Makefile0000664 0000000 0000000 00000000265 13204006733 0031542 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic5
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
__init__.py 0000664 0000000 0000000 00000000000 13204006733 0032117 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit/ui main_window.ui 0000664 0000000 0000000 00000014056 13204006733 0032700 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qwebcontentedit/ui
MainWindow00608609QWebcontenteditfalseLoad0EdittruePreviewtrueHistoryNew ColumnTimeAuthorSummaryQt::Horizontal4020Number of revisions10ReloadMinor editfalseNo changes0060822FiletoolBarTopToolBarAreafalseExitBackendsactionExittriggered()MainWindowclose()-1-1343306
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qweboobcfg/ 0000775 0000000 0000000 00000000000 13204006733 0026401 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qweboobcfg/__init__.py 0000664 0000000 0000000 00000001435 13204006733 0030515 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .qweboobcfg import QWeboobCfg
__all__ = ['QWeboobCfg']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/qweboobcfg/qweboobcfg.py 0000664 0000000 0000000 00000002450 13204006733 0031072 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.application.qt5 import BackendCfg, QtApplication
class QWeboobCfg(QtApplication):
APPNAME = 'qweboobcfg'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-2014 Romain Bignon'
DESCRIPTION = "weboob-config-qt is a graphical application to add/edit/remove backends, " \
"and to register new website accounts."
SHORT_DESCRIPTION = "manage backends or register new accounts"
def main(self, argv):
self.load_backends()
self.dlg = BackendCfg(self.weboob)
self.dlg.show()
return self.weboob.loop()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/radioob/ 0000775 0000000 0000000 00000000000 13204006733 0025702 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/radioob/__init__.py 0000664 0000000 0000000 00000001424 13204006733 0030014 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .radioob import Radioob
__all__ = ['Radioob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/radioob/radioob.py 0000664 0000000 0000000 00000037200 13204006733 0027675 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import subprocess
import os
import re
import requests
from weboob.capabilities.radio import CapRadio, Radio
from weboob.capabilities.audio import CapAudio, BaseAudio, Playlist, Album
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Radioob']
class RadioListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = ''
if hasattr(obj, 'description') and not empty(obj.description):
result += '%-30s' % obj.description
if hasattr(obj, 'current') and not empty(obj.current):
if obj.current.who:
result += ' (Current: %s - %s)' % (obj.current.who, obj.current.what)
else:
result += ' (Current: %s)' % obj.current.what
return result
class SongListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
result = obj.title
if hasattr(obj, 'author') and not empty(obj.author):
result += ' (%s)' % obj.author
return result
def get_description(self, obj):
result = ''
if hasattr(obj, 'description') and not empty(obj.description):
result += '%-30s' % obj.description
return result
class AlbumTrackListInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'tracks_list')
def get_title(self, obj):
result = obj.title
if hasattr(obj, 'author') and not empty(obj.author):
result += ' (%s)' % obj.author
return result
def get_description(self, obj):
result = ''
for song in obj.tracks_list:
result += '- %s%-30s%s ' % (self.BOLD, song.title, self.NC)
if hasattr(song, 'duration') and not empty(song.duration):
result += '%-10s ' % song.duration
else:
result += '%-10s ' % ' '
result += '(%s)\r\n\t' % (song.id)
return result
class PlaylistTrackListInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'tracks_list')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = ''
for song in obj.tracks_list:
result += '- %s%-30s%s ' % (self.BOLD, song.title, self.NC)
if hasattr(song, 'author') and not empty(song.author):
result += '(%-15s) ' % song.author
if hasattr(song, 'duration') and not empty(song.duration):
result += '%-10s ' % song.duration
else:
result += '%-10s ' % ' '
result += '(%s)\r\n\t' % (song.id)
return result
class Radioob(ReplApplication):
APPNAME = 'radioob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon\nCopyright(C) YEAR Pierre Maziere'
DESCRIPTION = "Console application allowing to search for web radio stations, listen to them and get information " \
"like the current song."
SHORT_DESCRIPTION = "search, show or listen to radio stations"
CAPS = (CapRadio, CapAudio)
EXTRA_FORMATTERS = {'radio_list': RadioListFormatter,
'song_list': SongListFormatter,
'album_tracks_list_info': AlbumTrackListInfoFormatter,
'playlist_tracks_list_info': PlaylistTrackListInfoFormatter,
}
COMMANDS_FORMATTERS = {'ls': 'radio_list',
'playlist': 'radio_list',
}
COLLECTION_OBJECTS = (Radio, BaseAudio, )
PLAYLIST = []
def __init__(self, *args, **kwargs):
super(Radioob, self).__init__(*args, **kwargs)
self.player = MediaPlayer(self.logger)
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def complete_download(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [DIRECTORY]
Download an audio file
"""
_id, dest = self.parse_command_args(line, 2, 1)
obj = self.retrieve_obj(_id)
if obj is None:
print('No object matches with this id:', _id, file=self.stderr)
return 3
if isinstance(obj, BaseAudio):
streams = [obj]
else:
streams = obj.tracks_list
if len(streams) == 0:
print('Radio or Audio file not found:', _id, file=self.stderr)
return 3
for stream in streams:
self.download_file(stream, dest)
def download_file(self, audio, dest):
_obj = self.get_object(audio.id, 'get_audio', ['url', 'title'])
if not _obj:
print('Audio file not found: %s' % audio.id, file=self.stderr)
return 3
if not _obj.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
audio.url = _obj.url
def check_exec(executable):
with open(os.devnull, 'w') as devnull:
process = subprocess.Popen(['which', executable], stdout=devnull)
if process.wait() != 0:
print('Please install "%s"' % executable, file=self.stderr)
return False
return True
def audio_to_file(_audio):
ext = _audio.ext
if not ext:
ext = 'audiofile'
title = _audio.title if _audio.title else _audio.id
return '%s.%s' % (re.sub('[?:/]', '-', title), ext)
if dest is not None and os.path.isdir(dest):
dest += '/%s' % audio_to_file(audio)
if dest is None:
dest = audio_to_file(audio)
if audio.url.startswith('rtmp'):
if not check_exec('rtmpdump'):
return 1
args = ('rtmpdump', '-e', '-r', audio.url, '-o', dest)
elif audio.url.startswith('mms'):
if not check_exec('mimms'):
return 1
args = ('mimms', '-r', audio.url, dest)
else:
if check_exec('wget'):
args = ('wget', '-c', audio.url, '-O', dest)
elif check_exec('curl'):
args = ('curl', '-C', '-', audio.url, '-o', dest)
else:
return 1
os.spawnlp(os.P_WAIT, args[0], *args)
def complete_play(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_play(self, line):
"""
play ID [stream_id]
Play a radio or a audio file with a found player (optionnaly specify the wanted stream).
"""
_id, stream_id = self.parse_command_args(line, 2, 1)
if not _id:
print('This command takes an argument: %s' % self.get_command_help('play', short=True), file=self.stderr)
return 2
try:
stream_id = int(stream_id)
except (ValueError, TypeError):
stream_id = 0
obj = self.retrieve_obj(_id)
if obj is None:
print('No object matches with this id:', _id, file=self.stderr)
return 3
if isinstance(obj, Radio):
try:
streams = [obj.streams[stream_id]]
except IndexError:
print('Stream %d not found' % stream_id, file=self.stderr)
return 1
elif isinstance(obj, BaseAudio):
streams = [obj]
else:
streams = obj.tracks_list
if len(streams) == 0:
print('Radio or Audio file not found:', _id, file=self.stderr)
return 3
try:
player_name = self.config.get('media_player')
media_player_args = self.config.get('media_player_args')
if not player_name:
self.logger.debug(u'You can set the media_player key to the player you prefer in the radioob '
'configuration file.')
for stream in streams:
if isinstance(stream, BaseAudio) and not stream.url:
stream = self.get_object(stream.id, 'get_audio')
else:
r = requests.get(stream.url, stream=True)
buf = next(r.iter_content(512)).decode('utf-8', 'replace')
r.close()
playlistFormat = None
for line in buf.split("\n"):
if playlistFormat is None:
if line == "[playlist]":
playlistFormat = "pls"
elif line == "#EXTM3U":
playlistFormat = "m3u"
else:
break
elif playlistFormat == "pls":
if line.startswith('File'):
stream.url = line.split('=', 1).pop(1).strip()
break
elif playlistFormat == "m3u":
if line[0] != "#":
stream.url = line.strip()
break
self.player.play(stream, player_name=player_name, player_args=media_player_args)
except (InvalidMediaPlayer, MediaPlayerNotFound) as e:
print('%s\nRadio URL: %s' % (e, stream.url))
def retrieve_obj(self, _id):
obj = None
if self.interactive:
try:
obj = self.objects[int(_id) - 1]
_id = obj.id
except (IndexError, ValueError):
pass
m = CapAudio.get_object_method(_id)
if m:
obj = self.get_object(_id, m)
return obj if obj is not None else self.get_object(_id, 'get_radio')
def do_playlist(self, line):
"""
playlist cmd [args]
playlist add ID [ID2 ID3 ...]
playlist remove ID [ID2 ID3 ...]
playlist export [FILENAME]
playlist display
"""
if not line:
print('This command takes an argument: %s' % self.get_command_help('playlist'), file=self.stderr)
return 2
cmd, args = self.parse_command_args(line, 2, req_n=1)
if cmd == "add":
_ids = args.strip().split(' ')
for _id in _ids:
audio = self.get_object(_id, 'get_audio')
if not audio:
print('Audio file not found: %s' % _id, file=self.stderr)
return 3
if not audio.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
self.PLAYLIST.append(audio)
elif cmd == "remove":
_ids = args.strip().split(' ')
for _id in _ids:
audio_to_remove = self.get_object(_id, 'get_audio')
if not audio_to_remove:
print('Audio file not found: %s' % _id, file=self.stderr)
return 3
if not audio_to_remove.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
for audio in self.PLAYLIST:
if audio.id == audio_to_remove.id:
self.PLAYLIST.remove(audio)
break
elif cmd == "export":
filename = "playlist.m3u"
if args:
filename = args
file = open(filename, 'w')
for audio in self.PLAYLIST:
file.write('%s\r\n' % audio.url)
file.close()
elif cmd == "display":
for audio in self.PLAYLIST:
self.cached_format(audio)
else:
print('Playlist command only support "add", "remove", "display" and "export" arguments.', file=self.stderr)
return 2
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a radio or an audio file.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
obj = self.retrieve_obj(_id)
if isinstance(obj, Album):
self.set_formatter('album_tracks_list_info')
elif isinstance(obj, Playlist):
self.set_formatter('playlist_tracks_list_info')
if obj is None:
print('No object matches with this id:', _id, file=self.stderr)
return 3
self.format(obj)
@defaultcount(10)
def do_search(self, pattern=None):
"""
search (radio|song|file|album|playlist) PATTERN
List (radio|song|file|album|playlist) matching a PATTERN.
If PATTERN is not given, this command will list all the (radio|song|album|playlist).
"""
if not pattern:
print('This command takes an argument: %s' % self.get_command_help('search'), file=self.stderr)
return 2
cmd, args = self.parse_command_args(pattern, 2, req_n=1)
if not args:
args = ""
self.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'All radios')
self.change_path([u'search'])
if cmd == "radio":
self.set_formatter('radio_list')
for radio in self.do('iter_radios_search', pattern=args):
self.add_object(radio)
self.format(radio)
elif cmd == "song" or cmd == "file":
self.set_formatter('song_list')
for audio in self.do('search_audio', pattern=args):
self.add_object(audio)
self.format(audio)
elif cmd == "album":
self.set_formatter('song_list')
for album in self.do('search_album', pattern=args):
self.add_object(album)
self.format(album)
elif cmd == "playlist":
self.set_formatter('song_list')
for playlist in self.do('search_playlist', pattern=args):
self.add_object(playlist)
self.format(playlist)
else:
print('Search command only supports "radio", "song", "file", "album" and "playlist" arguments.', file=self.stderr)
return 2
def do_ls(self, line):
"""
ls
List radios
"""
ret = super(Radioob, self).do_ls(line)
return ret
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/shopoob/ 0000775 0000000 0000000 00000000000 13204006733 0025734 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/shopoob/__init__.py 0000664 0000000 0000000 00000001424 13204006733 0030046 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .shopoob import Shopoob
__all__ = ['Shopoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/shopoob/shopoob.py 0000664 0000000 0000000 00000015044 13204006733 0027763 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2015 Christophe Lampin
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from decimal import Decimal
from weboob.capabilities.base import empty
from weboob.capabilities.shop import CapShop, Order, Item
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Shopoob']
class OrdersFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'total')
def start_format(self, **kwargs):
self.output(' Id Date Total ')
self.output('-----------------------------+------------+-----------')
def format_obj(self, obj, alias):
date = obj.date.strftime('%Y-%m-%d') if not empty(obj.date) else ''
total = obj.total or Decimal('0')
result = u'%s %s %s' % (self.colored('%-28s' % obj.fullid, 'yellow'),
self.colored('%-10s' % date, 'blue'),
self.colored('%9.2f' % total, 'green'))
return result
def flush(self):
self.output(u'----------------------------+------------+-----------')
class ItemsFormatter(IFormatter):
MANDATORY_FIELDS = ('label', 'url', 'price')
def start_format(self, **kwargs):
self.output(' Label Url Price ')
self.output('---------------------------------------------------------------------------+---------------------------------------------+----------')
def format_obj(self, obj, alias):
price = obj.price or Decimal('0')
result = u'%s %s %s' % (self.colored('%-75s' % obj.label[:75], 'yellow'),
self.colored('%-43s' % obj.url, 'magenta'),
self.colored('%9.2f' % price, 'green'))
return result
def flush(self):
self.output(u'---------------------------------------------------------------------------+---------------------------------------------+----------')
class PaymentsFormatter(IFormatter):
MANDATORY_FIELDS = ('date', 'method', 'amount')
def start_format(self, **kwargs):
self.output(' Date Method Amount ')
self.output('-----------+-----------------+----------')
def format_obj(self, obj, alias):
date = obj.date.strftime('%Y-%m-%d') if not empty(obj.date) else ''
amount = obj.amount or Decimal('0')
result = u'%s %s %s' % (self.colored('%-10s' % date, 'blue'),
self.colored('%-17s' % obj.method, 'yellow'),
self.colored('%9.2f' % amount, 'green'))
return result
def flush(self):
self.output(u'-----------+-----------------+----------')
class Shopoob(ReplApplication):
APPNAME = 'shopoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2015 Christophe Lampin'
DESCRIPTION = 'Console application to obtain details and status of e-commerce orders.'
SHORT_DESCRIPTION = "Obtain details and status of e-commerce orders"
CAPS = CapShop
COLLECTION_OBJECTS = (Order, )
EXTRA_FORMATTERS = {'orders': OrdersFormatter,
'items': ItemsFormatter,
'payments': PaymentsFormatter
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'orders': 'orders',
'items': 'items',
'payments': 'payments',
'ls': 'orders',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
@defaultcount(10)
def do_orders(self, line):
"""
orders [BACKEND_NAME]
Get orders of a backend.
If no BACKEND_NAME given, display all orders of all backends.
"""
if len(line) > 0:
backend_name = line
else:
backend_name = None
self.do_count(str(self.options.count)) # Avoid raise of MoreResultsAvailable
l = []
for order in self.do('iter_orders', backends=backend_name):
l.append(order)
self.start_format()
for order in sorted(l, self.comp_object):
self.format(order)
# Order by date DESC
def comp_object(self, obj1, obj2):
if obj1.date == obj2.date:
return 0
elif obj1.date < obj2.date:
return 1
else:
return -1
def do_items(self, id):
"""
items [ID]
Get items of orders.
"""
l = []
id, backend_name = self.parse_id(id, unique_backend=True)
if not id:
print('Error: please give a order ID (hint: use orders command)', file=self.stderr)
return 2
else:
l.append((id, backend_name))
for id, backend in l:
names = (backend,) if backend is not None else None
# TODO: Use specific formatter
mysum = Item()
mysum.label = u"Sum"
mysum.url = u"Generated by shopoob"
mysum.price = Decimal("0.")
self.start_format()
for item in self.do('iter_items', id, backends=names):
self.format(item)
mysum.price = item.price + mysum.price
self.format(mysum)
def do_payments(self, id):
"""
payments [ID]
Get payments of orders.
If no ID given, display payment of all backends.
"""
id, backend_name = self.parse_id(id, unique_backend=True)
if not id:
print('Error: please give a order ID (hint: use orders command)', file=self.stderr)
return 2
self.start_format()
for payment in self.do('iter_payments', id, backends=backend_name):
self.format(payment) woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/suboob/ 0000775 0000000 0000000 00000000000 13204006733 0025554 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/suboob/__init__.py 0000664 0000000 0000000 00000001415 13204006733 0027666 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .suboob import Suboob
__all__ = ['Suboob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/suboob/suboob.py 0000664 0000000 0000000 00000017050 13204006733 0027422 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.subtitle import CapSubtitle
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Suboob']
LANGUAGE_CONV = {
'ar': 'ara', 'eo': 'epo', 'ga': '', 'ru': 'rus',
'af': '', 'et': 'est', 'it': 'ita', 'sr': 'scc',
'sq': 'alb', 'tl': '', 'ja': 'jpn', 'sk': 'slo',
'hy': 'arm', 'fi': 'fin', 'kn': '', 'sl': 'slv',
'az': '', 'fr': 'fre', 'ko': 'kor', 'es': 'spa',
'eu': 'baq', 'gl': 'glg', 'la': '', 'sw': 'swa',
'be': '', 'ka': 'geo', 'lv': 'lav', 'sv': 'swe',
'bn': 'ben', 'de': 'ger', 'lt': 'lit', 'ta': '',
'bg': 'bul', 'gr': 'ell', 'mk': 'mac', 'te': 'tel',
'ca': 'cat', 'gu': '', 'ms': 'may', 'th': 'tha',
'zh': 'chi', 'ht': '', 'mt': '', 'tr': 'tur',
'hr': 'hrv', 'iw': 'heb', 'no': 'nor', 'uk': 'ukr',
'cz': 'cze', 'hi': 'hin', 'fa': 'per', 'ur': 'urd',
'da': 'dan', 'hu': 'hun', 'pl': 'pol', 'vi': 'vie',
'nl': 'dut', 'is': 'ice', 'pt': 'por', 'cy': '',
'en': 'eng', 'id': 'ind', 'ro': 'rum', 'yi': ''}
def sizeof_fmt(num):
for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
if num < 1024.0:
return "%-4.1f%s" % (num, x)
num /= 1024.0
class SubtitleInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name', 'url', 'description')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.name, self.NC)
result += 'ID: %s\n' % obj.fullid
result += 'URL: %s\n' % obj.url
if not empty(obj.language):
result += 'LANG: %s\n' % obj.language
if not empty(obj.nb_cd):
result += 'NB CD: %s\n' % obj.nb_cd
result += '\n%sDescription%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.description
return result
class SubtitleListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name')
def get_title(self, obj):
return obj.name
def get_description(self, obj):
result = u'lang : %s' % obj.language
result += ' ; %s CD' % obj.nb_cd
if not empty(obj.url):
result += ' ; url : %s' % obj.url
return result
class Suboob(ReplApplication):
APPNAME = 'suboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2013-YEAR Julien Veyssier'
DESCRIPTION = "Console application allowing to search for subtitles on various services " \
"and download them."
SHORT_DESCRIPTION = "search and download subtitles"
CAPS = CapSubtitle
EXTRA_FORMATTERS = {'subtitle_list': SubtitleListFormatter,
'subtitle_info': SubtitleInfoFormatter
}
COMMANDS_FORMATTERS = {'search': 'subtitle_list',
'info': 'subtitle_info'
}
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, id):
"""
info ID
Get information about a subtitle.
"""
subtitle = self.get_object(id, 'get_subtitle')
if not subtitle:
print('Subtitle not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(subtitle)
def complete_download(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [FILENAME]
Get the subtitle or archive file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
subtitle = self.get_object(id, 'get_subtitle')
if not subtitle:
print('Subtitle not found: %s' % id, file=self.stderr)
return 3
if dest is None:
ext = subtitle.ext
if empty(ext):
ext = 'zip'
dest = '%s.%s' % (subtitle.name, ext)
for buf in self.do('get_subtitle_file', subtitle.id, backends=subtitle.backend):
if buf:
if dest == '-':
self.stdout.write(buf)
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print('Unable to write file in "%s": %s' % (dest, e), file=self.stderr)
return 1
else:
print('Saved to %s' % dest)
return
@defaultcount(10)
def do_search(self, line):
"""
search language [PATTERN]
Search subtitles.
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
language, pattern = self.parse_command_args(line, 2, 1)
self.change_path([u'search'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for subtitle in self.do('iter_subtitles', language=language, pattern=pattern):
self.cached_format(subtitle)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/translaboob/ 0000775 0000000 0000000 00000000000 13204006733 0026571 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/translaboob/__init__.py 0000664 0000000 0000000 00000001434 13204006733 0030704 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Lucien Loiseau
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .translaboob import Translaboob
__all__ = ['Translaboob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/translaboob/translaboob.py 0000664 0000000 0000000 00000013407 13204006733 0031456 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Lucien Loiseau
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.translate import CapTranslate, TranslationFail, LanguageNotSupported
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Translaboob']
class TranslationFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'text')
def format_obj(self, obj, alias):
return u'%s* %s%s\n\t%s' % (self.BOLD, obj.backend, self.NC, obj.text.replace('\n', '\n\t'))
class XmlTranslationFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'text')
def start_format(self, **kwargs):
if 'source' in kwargs:
self.output('' % kwargs['source'])
def format_obj(self, obj, alias):
return u'\n%s\n' % (obj.backend, obj.text)
class Translaboob(ReplApplication):
APPNAME = 'translaboob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Lucien Loiseau'
DESCRIPTION = "Console application to translate text from one language to another"
SHORT_DESCRIPTION = "translate text from one language to another"
CAPS = CapTranslate
EXTRA_FORMATTERS = {'translation': TranslationFormatter,
'xmltrans': XmlTranslationFormatter,
}
COMMANDS_FORMATTERS = {'translate': 'translation',
}
LANGUAGE = {
'ar': 'Arabic', 'af': 'Afrikaans', 'sq': 'Albanian', 'hy': 'Armenian', 'az': 'Azerbaijani', 'eu': 'Basque', 'be': 'Belarusian',
'bn': 'Bengali', 'bg': 'Bulgarian', 'ca': 'Catalan', 'zh': 'Chinese', 'hr': 'Croatian', 'cz': 'Czech', 'da': 'Danish',
'nl': 'Dutch', 'en': 'English', 'eo': 'Esperanto', 'et': 'Estonian', 'tl': 'Filipino', 'fi': 'Finnish', 'fr': 'French',
'gl': 'Galician', 'ka': 'Georgian', 'de': 'German', 'gr': 'Greek', 'gu': 'Gujarati', 'ht': 'Haitian', 'iw': 'Hebrew',
'hi': 'Hindi', 'hu': 'Hungaric', 'is': 'Icelandic', 'id': 'Indonesian', 'ga': 'Irish', 'it': 'Italian', 'ja': 'Japanese',
'kn': 'Kannada', 'ko': 'Korean', 'la': 'Latin', 'lv': 'Latvian', 'lt': 'Lithuanian', 'mk': 'Macedonian', 'ms': 'Malay',
'mt': 'Maltese', 'no': 'Norwegian', 'fa': 'Persian', 'pl': 'Polish', 'pt': 'Portuguese', 'ro': 'Romanian', 'ru': 'Russian',
'sr': 'Serbian', 'sk': 'Slovak', 'sl': 'Slovenian', 'es': 'Spanish', 'sw': 'Swahili', 'sv': 'Swedish', 'ta': 'Tamil',
'te': 'Telugu', 'th': 'Thai', 'tr': 'Turkish', 'uk': 'Ukrainian', 'ur': 'Urdu', 'vi': 'Vietnamese', 'cy': 'Welsh', 'yi': 'Yiddish',
'nigger': 'Nigger!',
}
def do_translate(self, line):
"""
translate FROM TO [TEXT]
Translate from one language to another.
* FROM : source language
* TO : destination language
* TEXT : language to translate, standard input if - is given
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
lan_from, lan_to, text = self.parse_command_args(line, 3, 2)
try:
if lan_from not in self.LANGUAGE.keys():
raise LanguageNotSupported()
if lan_to not in self.LANGUAGE.keys():
raise LanguageNotSupported()
if not text or text == '-':
text = self.acquire_input()
self.start_format(source=text)
for translation in self.do('translate', self.LANGUAGE[lan_from], self.LANGUAGE[lan_to], text):
self.format(translation)
except (TranslationFail, LanguageNotSupported) as error:
print(error, file=self.stderr)
pass
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/traveloob/ 0000775 0000000 0000000 00000000000 13204006733 0026260 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/traveloob/__init__.py 0000664 0000000 0000000 00000001432 13204006733 0030371 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .traveloob import Traveloob
__all__ = ['Traveloob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/traveloob/traveloob.py 0000664 0000000 0000000 00000015226 13204006733 0030635 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Romain Bignon, Julien Hébert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import datetime
from weboob.capabilities.base import Currency, empty
from weboob.capabilities.travel import CapTravel, RoadmapFilters
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Traveloob']
class DeparturesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'type', 'departure_station', 'arrival_station', 'time')
def get_title(self, obj):
s = obj.type
if hasattr(obj, 'price') and not empty(obj.price):
s += u' %s %s' % (self.colored(u'—', 'cyan'), self.colored('%6.2f %s' % (obj.price, Currency.currency2txt(obj.currency)), 'green'))
if hasattr(obj, 'late') and not empty(obj.late) and obj.late > datetime.time():
s += u' %s %s' % (self.colored(u'—', 'cyan'), self.colored('Late: %s' % obj.late, 'red', 'bold'))
if hasattr(obj, 'information') and not empty(obj.information) and obj.information.strip() != '':
s += u' %s %s' % (self.colored(u'—', 'cyan'), self.colored(obj.information, 'red'))
return s
def get_description(self, obj):
if hasattr(obj, 'arrival_time') and not empty(obj.arrival_time):
s = '(%s) %s%s\n\t(%s) %s' % (self.colored(obj.time.strftime('%H:%M') if obj.time else '??:??', 'cyan'),
obj.departure_station,
self.colored(' [Platform: %s]' % obj.platform, 'yellow') if (hasattr(obj, 'platform') and not empty(obj.platform)) else '',
self.colored(obj.arrival_time.strftime('%H:%M'), 'cyan'),
obj.arrival_station)
else:
s = '(%s) %20s -> %s' % (self.colored(obj.time.strftime('%H:%M') if obj.time else '??:??', 'cyan'),
obj.departure_station, obj.arrival_station)
return s
class StationsFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name')
def get_title(self, obj):
return obj.name
class Traveloob(ReplApplication):
APPNAME = 'traveloob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to search for train stations and get departure times."
SHORT_DESCRIPTION = "search for train stations and departures"
CAPS = CapTravel
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'stations': StationsFormatter,
'departures': DeparturesFormatter,
}
COMMANDS_FORMATTERS = {'stations': 'stations',
'departures': 'departures',
}
def add_application_options(self, group):
group.add_option('--departure-time')
group.add_option('--arrival-time')
@defaultcount(10)
def do_stations(self, pattern):
"""
stations PATTERN
Search stations.
"""
for station in self.do('iter_station_search', pattern):
self.format(station)
@defaultcount(10)
def do_departures(self, line):
"""
departures STATION [ARRIVAL [DATE]]]
List all departures for a given station.
The format for the date is "yyyy-mm-dd HH:MM" or "HH:MM".
"""
station, arrival, date = self.parse_command_args(line, 3, 1)
station_id, backend_name = self.parse_id(station)
if arrival:
arrival_id, backend_name2 = self.parse_id(arrival)
if backend_name and backend_name2 and backend_name != backend_name2:
print('Departure and arrival aren\'t on the same backend', file=self.stderr)
return 1
else:
arrival_id = backend_name2 = None
if backend_name:
backends = [backend_name]
elif backend_name2:
backends = [backend_name2]
else:
backends = None
if date is not None:
try:
date = self.parse_datetime(date)
except ValueError as e:
print('Invalid datetime value: %s' % e, file=self.stderr)
print('Please enter a datetime in form "yyyy-mm-dd HH:MM" or "HH:MM".', file=self.stderr)
return 1
for departure in self.do('iter_station_departures', station_id, arrival_id, date, backends=backends):
self.format(departure)
def do_roadmap(self, line):
"""
roadmap DEPARTURE ARRIVAL
Display the roadmap to travel from DEPARTURE to ARRIVAL.
Command-line parameters:
--departure-time TIME requested departure time
--arrival-time TIME requested arrival time
TIME might be in form "yyyy-mm-dd HH:MM" or "HH:MM".
Example:
> roadmap Puteaux Aulnay-sous-Bois --arrival-time 22:00
"""
departure, arrival = self.parse_command_args(line, 2, 2)
filters = RoadmapFilters()
try:
filters.departure_time = self.parse_datetime(self.options.departure_time)
filters.arrival_time = self.parse_datetime(self.options.arrival_time)
except ValueError as e:
print('Invalid datetime value: %s' % e, file=self.stderr)
print('Please enter a datetime in form "yyyy-mm-dd HH:MM" or "HH:MM".', file=self.stderr)
return 1
for route in self.do('iter_roadmap', departure, arrival, filters):
self.format(route)
def parse_datetime(self, text):
if text is None:
return None
try:
date = datetime.datetime.strptime(text, '%Y-%m-%d %H:%M')
except ValueError:
try:
date = datetime.datetime.strptime(text, '%H:%M')
except ValueError:
raise ValueError(text)
date = datetime.datetime.now().replace(hour=date.hour, minute=date.minute)
return date
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/videoob/ 0000775 0000000 0000000 00000000000 13204006733 0025712 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/videoob/__init__.py 0000664 0000000 0000000 00000001426 13204006733 0030026 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .videoob import Videoob
__all__ = ['Videoob']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/videoob/image2xterm.py 0000664 0000000 0000000 00000011607 13204006733 0030515 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
#
# This file is distributed under the WTFPLv2 license.
from __future__ import division
import sys
import os
import PIL.Image as Image
import PIL.ImageColor as PILColor
from weboob.tools.compat import range
"""
XTerm can decode sequences and display 256 colors:
- 16 system colors, supported by many terms [0-15]
- 216 colors (not the web-safe palette) [16-231]
- 24 grey colors (excluding black and white) [231-255]
Other terminals do support these 256 colors escape codes, e.g. roxterm, xfce-terminal.
"""
__all__ = ('make256xterm', 'imageRGB_to_256', 'image2xterm', 'image256_to_ansi')
def make256xterm():
"""Return a [r, g, b, r, g, b, ...] table with the colors of the 256 xterm colors.
The table is indexed: [0:3] corresponds to color 0.
"""
color256 = []
# standard 16 colors
color256 += list(sum((PILColor.getrgb(c) for c in
'''black maroon green olive navy purple teal silver
gray red lime yellow blue fuchsia aqua white'''.split()), ()))
steps = (0, 95, 135, 175, 215, 255)
for r in steps:
for g in steps:
for b in steps:
color256 += [r, g, b]
# grays
for v in range(8, 248, 10):
color256 += [v, v, v]
assert len(color256) == 256 * 3
return color256
TABLE_XTERM_256 = make256xterm()
def imageRGB_to_256(im):
"""Returns `im` converted to the XTerm 256 colors palette.
The image should be resized *before* applying this function.
"""
paletter = Image.new('P', (1, 1))
paletter.putpalette(TABLE_XTERM_256)
return im.quantize(palette=paletter)
def image256_to_ansi(im, halfblocks):
"""Print PIL image `im` to `fd` using XTerm escape codes.
1 pixel corresponds to exactly 1 character on the terminal. 1 row of pixels is terminated by a newline.
`im` has to be a 256 colors image (ideally in the XTerm palette if you want it to make sense).
`im` should be resized appropriately to fit terminal size and character aspect (characters are never square).
"""
buf = []
pix = im.load()
w, h = im.size
if halfblocks:
yrange = range(0, h, 2)
else:
yrange = range(h)
for y in yrange:
for x in range(w):
if halfblocks:
if y + 1 >= h:
buf.append(u'\x1b[38;5;%dm\u2580' % pix[x, y])
else:
buf.append(u'\x1b[38;5;%dm\x1b[48;5;%dm\u2580' % (pix[x, y], pix[x, y + 1]))
else:
buf.append(u'\x1b[48;5;%dm ' % pix[x, y])
buf.append(u'\x1b[0m%s' % os.linesep)
return ''.join(buf)
def image2xterm(imagepath, newsize=(80, 24), halfblocks=True):
image = Image.open(imagepath)
image.load()
stretch = 2
curratio = image.size[1] / (image.size[0] * stretch)
targetsize = newsize[0], int(newsize[0] * curratio)
if targetsize[1] > newsize[1]:
targetsize = int(newsize[1] / curratio), newsize[1]
if halfblocks:
targetsize = targetsize[0], targetsize[1] * 2
image = image.convert('RGB')
image = image.resize(targetsize, Image.ANTIALIAS)
image2 = imageRGB_to_256(image)
return image256_to_ansi(image2, halfblocks=halfblocks)
def _getoutsize(infoname, default):
"""Get suitable dimension for tty or default"""
if sys.stdout.isatty():
import curses
curses.setupterm()
return curses.tigetnum(infoname)
else:
return default
def get_term_size():
return (_getoutsize('cols', 80), _getoutsize('lines', 24))
def main():
import argparse
parser = argparse.ArgumentParser(description='Print an image on terminal with 256 colors palette')
parser.add_argument('-r', '--rows', dest='rows', metavar='SIZE', type=int)
parser.add_argument('-c', '--columns', dest='cols', metavar='SIZE', type=int)
parser.add_argument('--spaces', action='store_true')
parser.add_argument('file')
opts = parser.parse_args()
if opts.rows is None:
opts.rows = _getoutsize('lines', 24)
if opts.cols is None:
opts.cols = _getoutsize('cols', 80)
data = image2xterm(opts.file, (opts.cols, opts.rows), halfblocks=not opts.spaces)
sys.stdout.write(data)
if __name__ == '__main__':
main()
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/videoob/videoob.py 0000664 0000000 0000000 00000030032 13204006733 0027711 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz, Romain Bignon, John Obbele, Nicolas Duhamel
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from io import BytesIO
import requests
import subprocess
import os
from weboob.capabilities.video import CapVideo, BaseVideo
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
from weboob.tools.compat import urlparse
from .image2xterm import image2xterm, get_term_size
__all__ = ['Videoob']
class VideoListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'duration', 'date')
DISPLAYED_FIELDS = ('author', 'rating', 'thumbnail')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
if empty(obj.duration) and empty(obj.date):
return None
result = '%s' % (obj.duration or obj.date)
if hasattr(obj, 'author') and not empty(obj.author):
result += u' - %s' % obj.author
if hasattr(obj, 'rating') and not empty(obj.rating):
result += u' (%s/%s)' % (obj.rating, obj.rating_max)
if hasattr(obj, 'thumbnail') and not empty(obj.thumbnail) and not empty(obj.thumbnail.data):
result += u'\n'
result += image2xterm(BytesIO(obj.thumbnail.data), newsize=get_term_size())
return result
class Videoob(ReplApplication):
APPNAME = 'videoob'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Christophe Benz, Romain Bignon, John Obbele'
DESCRIPTION = "Console application allowing to search for videos on various websites, " \
"play and download them and get information."
SHORT_DESCRIPTION = "search and play videos"
CAPS = CapVideo
EXTRA_FORMATTERS = {'video_list': VideoListFormatter}
COMMANDS_FORMATTERS = {'search': 'video_list',
'ls': 'video_list',
'playlist': 'video_list'}
COLLECTION_OBJECTS = (BaseVideo, )
PLAYLIST = []
nsfw = True
def __init__(self, *args, **kwargs):
super(Videoob, self).__init__(*args, **kwargs)
self.player = MediaPlayer(self.logger)
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def download(self, video, dest, default=None):
if not video.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
def check_exec(executable):
with open(os.devnull, 'w') as devnull:
process = subprocess.Popen(['which', executable], stdout=devnull)
if process.wait() != 0:
print('Please install "%s"' % executable, file=self.stderr)
return False
return True
dest = self.obj_to_filename(video, dest, default)
if video.url.startswith('rtmp'):
if not check_exec('rtmpdump'):
return 1
args = ('rtmpdump', '-e', '-r', video.url, '-o', dest)
elif video.url.startswith('mms'):
if not check_exec('mimms'):
return 1
args = ('mimms', '-r', video.url, dest)
elif u'm3u8' == video.ext:
_dest, _ = os.path.splitext(dest)
dest = u'%s.%s' % (_dest, 'mp4')
content = tuple()
parsed_uri = urlparse(video.url)
baseurl = '{uri.scheme}://{uri.netloc}'.format(uri=parsed_uri)
for line in self.read_url(video.url):
line = line.decode('utf-8')
if not line.startswith('#'):
if not line.startswith('http'):
line = u'%s%s' % (baseurl, line)
content += (line,)
args = ('wget', '-nv',) + content + ('-O', dest)
else:
if check_exec('wget'):
args = ('wget', '-c', video.url, '-O', dest)
elif check_exec('curl'):
args = ('curl', '-C', '-', video.url, '-o', dest)
else:
return 1
self.logger.debug(' '.join(args))
os.spawnlp(os.P_WAIT, args[0], *args)
def read_url(self, url):
r = requests.get(url, stream=True)
return r.iter_lines()
def complete_download(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [FILENAME]
Download a video
Braces-enclosed tags are replaced with data fields. Use the 'info'
command to see what fields are available on a given video.
Example: download KdRRge4XYIo@youtube '{title}.{ext}'
"""
_id, dest = self.parse_command_args(line, 2, 1)
video = self.get_object(_id, 'get_video', ['url'])
if not video:
print('Video not found: %s' % _id, file=self.stderr)
return 3
return self.download(video, dest)
def complete_play(self, text, line, *ignored):
args = line.split(' ')
if len(args) >= 2:
return self._complete_object()
def do_play(self, line):
"""
play ID
Play a video with a found player.
"""
if not line:
print('This command takes an argument: %s' % self.get_command_help('play', short=True), file=self.stderr)
return 2
ret = 0
for _id in line.split(' '):
video = self.get_object(_id, 'get_video', ['url'])
error = self.play(video, _id)
if error is not None:
ret = error
return ret
def play(self, video, _id):
if not video:
print('Video not found: %s' % _id, file=self.stderr)
return 3
if not video.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
try:
player_name = self.config.get('media_player')
media_player_args = self.config.get('media_player_args')
if not player_name:
self.logger.info(u'You can set the media_player key to the player you prefer in the videoob '
'configuration file.')
self.player.play(video, player_name=player_name, player_args=media_player_args)
except (InvalidMediaPlayer, MediaPlayerNotFound) as e:
print('%s\nVideo URL: %s' % (e, video.url))
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) >= 2:
return self._complete_object()
def do_info(self, line):
"""
info ID [ID2 [...]]
Get information about a video.
"""
if not line:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
self.start_format()
for _id in line.split(' '):
video = self.get_object(_id, 'get_video')
if not video:
print('Video not found: %s' % _id, file=self.stderr)
return 3
self.format(video)
def complete_playlist(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return ['play', 'add', 'remove', 'export', 'display', 'download']
if len(args) >= 3:
if args[1] in ('export', 'download'):
return self.path_completer(args[2])
if args[1] in ('add', 'remove'):
return self._complete_object()
def do_playlist(self, line):
"""
playlist cmd [args]
playlist add ID [ID2 ID3 ...]
playlist remove ID [ID2 ID3 ...]
playlist export [FILENAME]
playlist display
playlist download [PATH]
playlist play
"""
if not self.interactive:
print('This command can be used only in interactive mode.', file=self.stderr)
return 1
if not line:
print('This command takes an argument: %s' % self.get_command_help('playlist'), file=self.stderr)
return 2
cmd, args = self.parse_command_args(line, 2, req_n=1)
if cmd == "add":
_ids = args.strip().split(' ')
for _id in _ids:
video = self.get_object(_id, 'get_video')
if not video:
print('Video not found: %s' % _id, file=self.stderr)
return 3
if not video.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
self.PLAYLIST.append(video)
elif cmd == "remove":
_ids = args.strip().split(' ')
for _id in _ids:
video_to_remove = self.get_object(_id, 'get_video')
if not video_to_remove:
print('Video not found: %s' % _id, file=self.stderr)
return 3
if not video_to_remove.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
for video in self.PLAYLIST:
if video.id == video_to_remove.id:
self.PLAYLIST.remove(video)
break
elif cmd == "export":
filename = "playlist.m3u"
if args:
filename = args
file = open(filename, 'w')
for video in self.PLAYLIST:
file.write('%s\r\n' % video.url)
file.close()
elif cmd == "display":
for video in self.PLAYLIST:
self.cached_format(video)
elif cmd == "download":
for i, video in enumerate(self.PLAYLIST):
self.download(video, args, '%02d-{id}-{title}.{ext}' % (i+1))
elif cmd == "play":
for video in self.PLAYLIST:
self.play(video, video.id)
else:
print('Playlist command only support "add", "remove", "display", "download" and "export" arguments.', file=self.stderr)
return 2
def complete_nsfw(self, text, line, begidx, endidx):
return ['on', 'off']
def do_nsfw(self, line):
"""
nsfw [on | off]
If argument is given, enable or disable the non-suitable for work behavior.
If no argument is given, print the current behavior.
"""
line = line.strip()
if line:
if line == 'on':
self.nsfw = True
elif line == 'off':
self.nsfw = False
else:
print('Invalid argument "%s".' % line)
return 2
else:
print("on" if self.nsfw else "off")
@defaultcount()
def do_search(self, pattern):
"""
search PATTERN
Search for videos matching a PATTERN.
"""
if not pattern:
print('This command takes an argument: %s' % self.get_command_help('search', short=True), file=self.stderr)
return 2
self.change_path([u'search'])
self.start_format(pattern=pattern)
for video in self.do('search_videos', pattern=pattern, nsfw=self.nsfw):
self.cached_format(video)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/webcontentedit/ 0000775 0000000 0000000 00000000000 13204006733 0027301 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/webcontentedit/__init__.py 0000664 0000000 0000000 00000001451 13204006733 0031413 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .webcontentedit import WebContentEdit
__all__ = ['WebContentEdit']
webcontentedit.py 0000664 0000000 0000000 00000016461 13204006733 0032622 0 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/webcontentedit # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import tempfile
import codecs
from distutils.spawn import find_executable
from weboob.core.bcall import CallErrors
from weboob.capabilities.content import CapContent, Revision
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.compat import unicode
__all__ = ['WebContentEdit']
class WebContentEdit(ReplApplication):
APPNAME = 'webcontentedit'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to display and edit contents on various websites."
SHORT_DESCRIPTION = "manage websites content"
CAPS = CapContent
def do_edit(self, line):
"""
edit ID [ID...]
Edit a content with $EDITOR, then push it on the website.
"""
contents = []
for id in line.split():
_id, backend_name = self.parse_id(id, unique_backend=True)
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
contents += [content for content in self.do('get_content', _id, backends=backend_names) if content]
if len(contents) == 0:
print('No contents found', file=self.stderr)
return 3
if self.stdin.isatty():
paths = {}
for content in contents:
tmpdir = os.path.join(tempfile.gettempdir(), "weboob")
if not os.path.isdir(tmpdir):
os.makedirs(tmpdir)
with tempfile.NamedTemporaryFile(prefix='%s_' % content.id.replace(os.path.sep, '_'), dir=tmpdir, delete=False) as f:
data = content.content
if isinstance(data, unicode):
data = data.encode('utf-8')
elif data is None:
content.content = u''
data = ''
f.write(data)
paths[f.name.encode('utf-8')] = content
params = ''
editor = os.environ.get('EDITOR', 'vim')
# check cases where /usr/bin/vi is a symlink to vim
if 'vim' in (os.path.basename(editor), os.path.basename(os.path.realpath(find_executable(editor) or '/')).replace('.nox', '')):
params = '-p'
os.system("%s %s %s" % (editor, params, ' '.join('"%s"' % path.replace('"', '\\"') for path in paths)))
for path, content in paths.items():
with open(path, 'r') as f:
data = f.read()
try:
data = data.decode('utf-8')
except UnicodeError:
pass
if content.content != data:
content.content = data
else:
contents.remove(content)
if len(contents) == 0:
print('No changes. Abort.', file=self.stderr)
return 1
print('Contents changed:\n%s' % ('\n'.join(' * %s' % content.id for content in contents)))
message = self.ask('Enter a commit message', default='')
minor = self.ask('Is this a minor edit?', default=False)
if not self.ask('Do you want to push?', default=True):
return
errors = CallErrors([])
for content in contents:
path = [path for path, c in paths.items() if c == content][0]
self.stdout.write('Pushing %s...' % content.id.encode('utf-8'))
self.stdout.flush()
try:
self.do('push_content', content, message, minor=minor, backends=[content.backend]).wait()
except CallErrors as e:
errors.errors += e.errors
self.stdout.write(' error (content saved in %s)\n' % path)
else:
self.stdout.write(' done\n')
os.unlink(path)
else:
# stdin is not a tty
if len(contents) != 1:
print("Multiple ids not supported with pipe", file=self.stderr)
return 2
message, minor = '', False
data = self.stdin.read()
contents[0].content = data.decode(self.guess_encoding(self.stdin))
errors = CallErrors([])
for content in contents:
self.stdout.write('Pushing %s...' % content.id.encode(self.encoding))
self.stdout.flush()
try:
self.do('push_content', content, message, minor=minor, backends=[content.backend]).wait()
except CallErrors as e:
errors.errors += e.errors
self.stdout.write(' error\n')
else:
self.stdout.write(' done\n')
if len(errors.errors) > 0:
raise errors
@defaultcount(10)
def do_log(self, line):
"""
log ID
Display log of a page
"""
if not line:
print('Error: please give a page ID', file=self.stderr)
return 2
_id, backend_name = self.parse_id(line)
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
_id = _id.encode('utf-8')
self.start_format()
for revision in self.do('iter_revisions', _id, backends=backend_names):
self.format(revision)
def do_get(self, line):
"""
get ID [-r revision]
Get page contents
"""
if not line:
print('Error: please give a page ID', file=self.stderr)
return 2
_part_line = line.strip().split(' ')
revision = None
if '-r' in _part_line:
r_index = _part_line.index('-r')
if len(_part_line) -1 > r_index:
revision = Revision(_part_line[r_index+1])
_part_line.remove(revision.id)
_part_line.remove('-r')
if not _part_line:
print('Error: please give a page ID', file=self.stderr)
return 2
_id, backend_name = self.parse_id(" ".join(_part_line))
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
_id = _id.encode('utf-8')
output = codecs.getwriter(self.encoding)(self.stdout)
for contents in [content for content in self.do('get_content', _id, revision, backends=backend_names) if content]:
output.write(contents.content)
# add a newline unless we are writing
# in a file or in a pipe
if output.isatty():
output.write('\n')
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobcfg/ 0000775 0000000 0000000 00000000000 13204006733 0026220 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobcfg/__init__.py 0000664 0000000 0000000 00000001432 13204006733 0030331 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobcfg import WeboobCfg
__all__ = ['WeboobCfg']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobcfg/weboobcfg.py 0000664 0000000 0000000 00000026665 13204006733 0030546 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import re
from collections import OrderedDict
from weboob.capabilities.account import CapAccount
from weboob.exceptions import ModuleLoadError
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.console import ConsoleProgress
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['WeboobCfg']
class CapabilitiesWrapper(list):
"""
A wrapper class to keep the list nature of capabilities,
but provide a comma separated list representation for
formaters unable to display a list by themselves.
Useful for having an array representation in JSON and
comma separated list for simple format.
"""
def __repr__(self):
return ', '.join(self)
class ModuleInfoFormatter(IFormatter):
def format_dict(self, minfo):
result = '.------------------------------------------------------------------------------.\n'
result += '| Module %-69s |\n' % minfo['name']
result += "+-----------------.------------------------------------------------------------'\n"
result += '| Version | %s\n' % minfo['version']
result += '| Maintainer | %s\n' % minfo['maintainer']
result += '| License | %s\n' % minfo['license']
result += '| Description | %s\n' % minfo['description']
result += '| Capabilities | %s\n' % ', '.join(minfo['capabilities'])
result += '| Installed | %s\n' % minfo['installed']
result += '| Location | %s\n' % minfo['location']
if 'config' in minfo:
first = True
for key, field in minfo['config'].items():
label = field['label']
if field['default'] is not None:
label += ' (default: %s)' % field['default']
if first:
result += '| | \n'
result += '| Configuration | %s: %s\n' % (key, label)
first = False
else:
result += '| | %s: %s\n' % (key, label)
result += "'-----------------'\n"
return result
class WeboobCfg(ReplApplication):
APPNAME = 'weboob-config'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Christophe Benz, Romain Bignon'
DESCRIPTION = "Weboob-Config is a console application to add/edit/remove backends, " \
"and to register new website accounts."
SHORT_DESCRIPTION = "manage backends or register new accounts"
EXTRA_FORMATTERS = {'info_formatter': ModuleInfoFormatter}
COMMANDS_FORMATTERS = {'modules': 'table',
'list': 'table',
'info': 'info_formatter',
}
DISABLE_REPL = True
def load_default_backends(self):
pass
def do_add(self, line):
"""
add MODULE_NAME [BACKEND_NAME] [PARAMETERS ...]
Create a backend from a module. By default, if BACKEND_NAME is omitted,
that's the module name which is used.
You can specify parameters from command line in form "key=value".
"""
if not line:
print('You must specify a module name. Hint: use the "modules" command.', file=self.stderr)
return 2
module_name, options = self.parse_command_args(line, 2, 1)
if options:
options = options.split(' ')
else:
options = ()
backend_name = None
params = {}
# set backend params from command-line arguments
for option in options:
try:
key, value = option.split('=', 1)
except ValueError:
if backend_name is None:
backend_name = option
else:
print('Parameters have to be formatted "key=value"', file=self.stderr)
return 2
else:
params[key] = value
self.add_backend(module_name, backend_name or module_name, params)
def do_register(self, line):
"""
register MODULE
Register a new account on a module.
"""
self.register_backend(line)
def do_confirm(self, backend_name):
"""
confirm BACKEND
For a backend which support CapAccount, parse a confirmation mail
after using the 'register' command to automatically confirm the
subscribe.
It takes mail from stdin. Use it with postfix for example.
"""
# Do not use the ReplApplication.load_backends() method because we
# don't want to prompt user to create backend.
self.weboob.load_backends(names=[backend_name])
try:
backend = self.weboob.get_backend(backend_name)
except KeyError:
print('Error: backend "%s" not found.' % backend_name, file=self.stderr)
return 1
if not backend.has_caps(CapAccount):
print('Error: backend "%s" does not support accounts management' % backend_name, file=self.stderr)
return 1
mail = self.acquire_input()
if not backend.confirm_account(mail):
print('Error: Unable to confirm account creation', file=self.stderr)
return 1
return 0
def do_list(self, line):
"""
list [CAPS ..]
Show backends.
"""
caps = line.split()
for backend_name, module_name, params in sorted(self.weboob.backends_config.iter_backends()):
try:
module = self.weboob.modules_loader.get_or_load_module(module_name)
except ModuleLoadError as e:
self.logger.warning('Unable to load module %r: %s' % (module_name, e))
continue
if caps and not module.has_caps(*caps):
continue
row = OrderedDict([('Name', backend_name),
('Module', module_name),
('Configuration', ', '.join(
'%s=%s' % (key, ('*****' if key in module.config and module.config[key].masked
else value))
for key, value in params.items())),
])
self.format(row)
def do_remove(self, backend_name):
"""
remove NAME
Remove a backend.
"""
if not self.weboob.backends_config.remove_backend(backend_name):
print('Backend instance "%s" does not exist' % backend_name, file=self.stderr)
return 1
def _do_toggle(self, backend_name, state):
try:
module_name, items = self.weboob.backends_config.get_backend(backend_name)
except KeyError:
print('Backend instance "%s" does not exist' % backend_name, file=self.stderr)
return 1
self.weboob.backends_config.edit_backend(backend_name, module_name, {'_enabled': state})
def do_enable(self, backend_name):
"""
enable BACKEND
Enable a disabled backend
"""
return self._do_toggle(backend_name, "true")
def do_disable(self, backend_name):
"""
disable BACKEND
Disable a backend
"""
return self._do_toggle(backend_name, "false")
def do_edit(self, line):
"""
edit BACKEND
Edit a backend
"""
try:
self.edit_backend(line)
except KeyError:
print('Error: backend "%s" not found' % line, file=self.stderr)
return 1
def do_modules(self, line):
"""
modules [CAPS ...]
Show available modules.
"""
caps = line.split()
for name, info in sorted(self.weboob.repositories.get_all_modules_info(caps).items()):
row = OrderedDict([('Name', name),
('Capabilities', CapabilitiesWrapper(info.capabilities)),
('Description', info.description),
('Installed', info.is_installed()),
])
self.format(row)
def do_info(self, line):
"""
info NAME
Display information about a module.
"""
if not line:
print('You must specify a module name. Hint: use the "modules" command.', file=self.stderr)
return 2
minfo = self.weboob.repositories.get_module_info(line)
if not minfo:
print('Module "%s" does not exist.' % line, file=self.stderr)
return 1
try:
module = self.weboob.modules_loader.get_or_load_module(line)
except ModuleLoadError:
module = None
self.start_format()
self.format(self.create_minfo_dict(minfo, module))
def create_minfo_dict(self, minfo, module):
module_info = {}
module_info['name'] = minfo.name
module_info['version'] = minfo.version
module_info['maintainer'] = minfo.maintainer
module_info['license'] = minfo.license
module_info['description'] = minfo.description
module_info['capabilities'] = minfo.capabilities
module_info['installed'] = '%s%s' % (('yes' if module else 'no'), ' (new version available)' if self.weboob.repositories.versions.get(minfo.name) > minfo.version else '')
module_info['location'] = '%s' % (minfo.url or os.path.join(minfo.path, minfo.name))
if module:
module_info['config'] = {}
for key, field in module.config.items():
module_info['config'][key] = {'label': field.label,
'default': field.default,
'description': field.description,
'regexp': field.regexp,
'choices': field.choices,
'masked': field.masked,
'required': field.required}
return module_info
def do_applications(self, line):
"""
applications
Show applications.
"""
applications = set()
import weboob.applications
for path in weboob.applications.__path__:
regexp = re.compile('^%s/([\w\d_]+)$' % path)
for root, dirs, files in os.walk(path):
m = regexp.match(root)
if m and '__init__.py' in files:
applications.add(m.group(1))
print(' '.join(sorted(applications)).encode('utf-8'))
def do_update(self, line):
"""
update
Update weboob.
"""
self.weboob.update(ConsoleProgress(self))
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobcli/ 0000775 0000000 0000000 00000000000 13204006733 0026230 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobcli/__init__.py 0000664 0000000 0000000 00000001432 13204006733 0030341 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobcli import WeboobCli
__all__ = ['WeboobCli']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobcli/weboobcli.py 0000664 0000000 0000000 00000003412 13204006733 0030547 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.tools.application.repl import ReplApplication
__all__ = ['WeboobCli']
class WeboobCli(ReplApplication):
APPNAME = 'weboob-cli'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
SYNOPSIS = 'Usage: %prog [-dqv] [-b backends] [-cnfs] capability method [arguments..]\n'
SYNOPSIS += ' %prog [--help] [--version]'
DESCRIPTION = "Weboob-Cli is a console application to call a specific method on backends " \
"which implement the given capability."
SHORT_DESCRIPTION = "call a method on backends"
DISABLE_REPL = True
def load_default_backends(self):
pass
def main(self, argv):
if len(argv) < 3:
print("Syntax: %s capability method [args ..]" % argv[0], file=self.stderr)
return 2
cap_s = argv[1]
cmd = argv[2]
args = argv[3:]
self.load_backends(cap_s)
for obj in self.do(cmd, *args):
self.format(obj)
self.flush()
return 0
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobdebug/ 0000775 0000000 0000000 00000000000 13204006733 0026547 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobdebug/__init__.py 0000664 0000000 0000000 00000001441 13204006733 0030660 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobdebug import WeboobDebug
__all__ = ['WeboobDebug']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobdebug/weboobdebug.py 0000664 0000000 0000000 00000006540 13204006733 0031412 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from optparse import OptionGroup
from weboob.tools.application.base import Application
class WeboobDebug(Application):
APPNAME = 'weboobdebug'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Christophe Benz'
DESCRIPTION = "Weboob-Debug is a console application to debug backends."
SHORT_DESCRIPTION = "debug backends"
def __init__(self, option_parser=None):
super(WeboobDebug, self).__init__(option_parser)
options = OptionGroup(self._parser, 'Weboob-Debug options')
options.add_option('-B', '--bpython', action='store_true', help='Prefer bpython over ipython')
self._parser.add_option_group(options)
def load_default_backends(self):
pass
def main(self, argv):
"""
BACKEND
Debug BACKEND.
"""
try:
backend_name = argv[1]
except IndexError:
print('Usage: %s BACKEND' % argv[0], file=self.stderr)
return 1
try:
backend = self.weboob.load_backends(names=[backend_name])[backend_name]
except KeyError:
print(u'Unable to load backend "%s"' % backend_name, file=self.stderr)
return 1
locs = dict(backend=backend, browser=backend.browser, application=self, weboob=self.weboob)
banner = 'Weboob debug shell\nBackend "%s" loaded.\nAvailable variables:\n' % backend_name \
+ '\n'.join([' %s: %s' % (k, v) for k, v in locs.items()])
if self.options.bpython:
funcs = [self.bpython, self.ipython, self.python]
else:
funcs = [self.ipython, self.bpython, self.python]
for func in funcs:
try:
func(locs, banner)
except ImportError:
continue
else:
break
def ipython(self, locs, banner):
try:
from IPython import embed
embed(user_ns=locs, banner2=banner)
except ImportError:
from IPython.Shell import IPShellEmbed
shell = IPShellEmbed(argv=[])
shell.set_banner(shell.IP.BANNER + '\n\n' + banner)
shell(local_ns=locs, global_ns={})
def bpython(self, locs, banner):
from bpython import embed
embed(locs, banner=banner)
def python(self, locs, banner):
import code
try:
import readline
import rlcompleter
readline.set_completer(rlcompleter.Completer(locs).complete)
readline.parse_and_bind("tab:complete")
except ImportError:
pass
code.interact(banner=banner, local=locs)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobrepos/ 0000775 0000000 0000000 00000000000 13204006733 0026611 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobrepos/__init__.py 0000664 0000000 0000000 00000001440 13204006733 0030721 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobrepos import WeboobRepos
__all__ = ['WeboobRepos']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboobrepos/weboobrepos.py 0000664 0000000 0000000 00000021211 13204006733 0031506 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import datetime
from time import mktime, strptime
import tarfile
import os
import shutil
import subprocess
from copy import copy
from contextlib import closing
from weboob.core.repositories import Repository
from weboob.tools.application.repl import ReplApplication
from weboob.tools.misc import find_exe
__all__ = ['WeboobRepos']
class WeboobRepos(ReplApplication):
APPNAME = 'weboob-repos'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2012-YEAR Romain Bignon'
DESCRIPTION = "Weboob-repos is a console application to manage a Weboob Repository."
SHORT_DESCRIPTION = "manage a weboob repository"
COMMANDS_FORMATTERS = {'backends': 'table',
'list': 'table',
}
DISABLE_REPL = True
weboob_commands = copy(ReplApplication.weboob_commands)
weboob_commands.remove('backends')
def load_default_backends(self):
pass
def do_create(self, line):
"""
create NAME [PATH]
Create a new repository. If PATH is missing, create repository
on the current directory.
"""
name, path = self.parse_command_args(line, 2, 1)
if not path:
path = os.getcwd()
else:
path = os.path.realpath(path)
if not os.path.exists(path):
os.mkdir(path)
elif not os.path.isdir(path):
print(u'"%s" is not a directory' % path)
return 1
r = Repository('http://')
r.name = name
r.maintainer = self.ask('Enter maintainer of the repository')
r.save(os.path.join(path, r.INDEX))
print(u'Repository "%s" created.' % path)
def do_build(self, line):
"""
build SOURCE REPOSITORY
Build backends contained in SOURCE to REPOSITORY.
Example:
$ weboob-repos build $HOME/src/weboob/modules /var/www/updates.weboob.org/0.a/
"""
source_path, repo_path = self.parse_command_args(line, 2, 2)
index_file = os.path.join(repo_path, Repository.INDEX)
r = Repository('http://')
try:
with open(index_file, 'r') as fp:
r.parse_index(fp)
except IOError as e:
print('Unable to open repository: %s' % e, file=self.stderr)
print('Use the "create" command before.', file=self.stderr)
return 1
r.build_index(source_path, index_file)
if r.signed:
sigfiles = [r.KEYRING, Repository.INDEX]
gpg = find_exe('gpg1') or find_exe('gpg')
if not gpg:
raise Exception('Unable to find the gpg executable.')
krname = os.path.join(repo_path, r.KEYRING)
if os.path.exists(krname):
kr_mtime = int(datetime.fromtimestamp(os.path.getmtime(krname)).strftime('%Y%m%d%H%M'))
if not os.path.exists(krname) or kr_mtime < r.key_update:
print('Generate keyring')
# Remove all existing keys
if os.path.exists(krname):
os.remove(krname)
# Add all valid keys
for keyfile in os.listdir(os.path.join(source_path, r.KEYDIR)):
print('Adding key %s' % keyfile)
keypath = os.path.join(source_path, r.KEYDIR, keyfile)
subprocess.check_call([
gpg,
'--no-options',
'--quiet',
'--no-default-keyring',
'--keyring', os.path.realpath(krname),
'--import', os.path.realpath(keypath)])
# Does not make much sense in our case
if os.path.exists(krname + '~'):
os.remove(krname + '~')
if not os.path.exists(krname):
raise Exception('No valid key file found.')
kr_mtime = mktime(strptime(str(r.key_update), '%Y%m%d%H%M'))
os.chmod(krname, 0o644)
os.utime(krname, (kr_mtime, kr_mtime))
else:
print('Keyring is up to date')
for name, module in r.modules.items():
tarname = os.path.join(repo_path, '%s.tar.gz' % name)
if r.signed:
sigfiles.append(os.path.basename(tarname))
module_path = os.path.join(source_path, name)
if os.path.exists(tarname):
tar_mtime = int(datetime.fromtimestamp(os.path.getmtime(tarname)).strftime('%Y%m%d%H%M'))
if tar_mtime >= module.version:
continue
print('Create archive for %s' % name)
with closing(tarfile.open(tarname, 'w:gz')) as tar:
tar.add(module_path, arcname=name, exclude=self._archive_excludes)
tar_mtime = mktime(strptime(str(module.version), '%Y%m%d%H%M'))
os.utime(tarname, (tar_mtime, tar_mtime))
# Copy icon.
icon_path = os.path.join(module_path, 'favicon.png')
if os.path.exists(icon_path):
shutil.copy(icon_path, os.path.join(repo_path, '%s.png' % name))
if r.signed:
gpg = find_exe('gpg2') or find_exe('gpg')
if not gpg:
raise Exception('Unable to find the gpg executable.')
# Find out which keys are allowed to sign
fingerprints = [gpgline.strip(':').split(':')[-1]
for gpgline
in subprocess.Popen([
gpg,
'--no-options',
'--with-fingerprint', '--with-colons',
'--list-public-keys',
'--no-default-keyring',
'--keyring', os.path.realpath(krname)],
stdout=subprocess.PIPE).communicate()[0].splitlines()
if gpgline.startswith('fpr:')]
# Find out the first secret key we have that is allowed to sign
secret_fingerprint = None
for fingerprint in fingerprints:
proc = subprocess.Popen([
gpg,
'--no-options',
'--list-secret-keys', fingerprint],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.communicate()
# if failed
if proc.returncode:
continue
secret_fingerprint = fingerprint
if secret_fingerprint is None:
raise Exception('No suitable secret key found')
# Check if all files have an up to date signature
for filename in sigfiles:
filepath = os.path.realpath(os.path.join(repo_path, filename))
sigpath = filepath + '.sig'
file_mtime = int(os.path.getmtime(filepath))
if os.path.exists(sigpath):
sig_mtime = int(os.path.getmtime(sigpath))
if not os.path.exists(sigpath) or sig_mtime < file_mtime:
print('Signing %s' % filename)
if os.path.exists(sigpath):
os.remove(sigpath)
subprocess.check_call([
gpg,
'--no-options',
'--quiet',
'--local-user', secret_fingerprint,
'--detach-sign',
'--output', sigpath,
'--sign', filepath])
os.utime(sigpath, (file_mtime, file_mtime))
print('Signatures are up to date')
def _archive_excludes(self, filename):
# Skip *.pyc files in tarballs.
if filename.endswith('.pyc'):
return True
# Don't include *.png files in tarball
if filename.endswith('.png'):
return True
return False
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboorrents/ 0000775 0000000 0000000 00000000000 13204006733 0026634 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboorrents/__init__.py 0000664 0000000 0000000 00000001440 13204006733 0030744 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboorrents import Weboorrents
__all__ = ['Weboorrents']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/weboorrents/weboorrents.py 0000664 0000000 0000000 00000015011 13204006733 0031555 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.torrent import CapTorrent, MagnetOnly
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.core import CallErrors
from weboob.capabilities.base import NotAvailable, NotLoaded, empty
__all__ = ['Weboorrents']
def sizeof_fmt(num):
for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
if num < 1024.0:
return "%-4.1f%s" % (num, x)
num /= 1024.0
class TorrentInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name', 'size', 'seeders', 'leechers', 'url', 'files', 'description')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.name, self.NC)
result += 'ID: %s\n' % obj.fullid
if obj.size != NotAvailable and obj.size != NotLoaded:
result += 'Size: %s\n' % sizeof_fmt(obj.size)
result += 'Seeders: %s\n' % obj.seeders
result += 'Leechers: %s\n' % obj.leechers
result += 'URL: %s\n' % obj.url
if hasattr(obj, 'magnet') and obj.magnet:
result += 'Magnet URL: %s\n' % obj.magnet
if obj.files:
result += '\n%sFiles%s\n' % (self.BOLD, self.NC)
for f in obj.files:
result += ' * %s\n' % f
result += '\n%sDescription%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.description
return result
class TorrentListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'size', 'seeders', 'leechers')
def get_title(self, obj):
return obj.name
NB2COLOR = ((0, 'red', None),
(1, 'blue', None),
(5, 'green', None),
(10, 'green', 'bold'),
)
def _get_color(self, nb):
if empty(nb):
return self.colored('N/A', 'red')
for threshold, _color, _attr in self.NB2COLOR:
if nb >= threshold:
color = _color
attr = _attr
return self.colored('%3d' % nb, color, attr)
def get_description(self, obj):
size = self.colored('%10s' % sizeof_fmt(obj.size), 'magenta')
return '%s (Seed: %s / Leech: %s)' % (size,
self._get_color(obj.seeders),
self._get_color(obj.leechers))
class Weboorrents(ReplApplication):
APPNAME = 'weboorrents'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to search for torrents on various trackers " \
"and download .torrent files."
SHORT_DESCRIPTION = "search and download torrents"
CAPS = CapTorrent
EXTRA_FORMATTERS = {'torrent_list': TorrentListFormatter,
'torrent_info': TorrentInfoFormatter,
}
COMMANDS_FORMATTERS = {'search': 'torrent_list',
'info': 'torrent_info',
}
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, id):
"""
info ID
Get information about a torrent.
"""
torrent = self.get_object(id, 'get_torrent', ('id', 'name', 'size', 'seeders', 'leechers', 'url', 'files', 'description'))
if not torrent:
print('Torrent not found: %s' % id, file=self.stderr)
return 3
self.start_format()
self.format(torrent)
def complete_getfile(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_getfile(self, line):
"""
getfile ID [FILENAME]
Get the .torrent file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
torrent = self.get_object(id, 'get_torrent', ('description', 'files'))
if not torrent:
print('Torrent not found: %s' % id, file=self.stderr)
return 3
dest = self.obj_to_filename(torrent, dest, '{id}-{name}.torrent')
try:
for buf in self.do('get_torrent_file', torrent.id, backends=torrent.backend):
if buf:
if dest == '-':
print(buf)
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print('Unable to write .torrent in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MagnetOnly):
print(u'Error(%s): No direct URL available, '
u'please provide this magnet URL '
u'to your client:\n%s' % (backend, error.magnet), file=self.stderr)
return 4
else:
self.bcall_error_handler(backend, error, backtrace)
print('Torrent "%s" not found' % id, file=self.stderr)
return 3
@defaultcount(10)
def do_search(self, pattern):
"""
search [PATTERN]
Search torrents.
"""
self.change_path([u'search'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for torrent in self.do('iter_torrents', pattern=pattern):
self.cached_format(torrent)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/wetboobs/ 0000775 0000000 0000000 00000000000 13204006733 0026107 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/wetboobs/__init__.py 0000664 0000000 0000000 00000001427 13204006733 0030224 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .wetboobs import WetBoobs
__all__ = ['WetBoobs']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/applications/wetboobs/wetboobs.py 0000664 0000000 0000000 00000011330 13204006733 0030303 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.weather import CapWeather
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['WetBoobs']
class ForecastsFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'low', 'high')
temperature_display = staticmethod(lambda t: u'%s' % t.value)
def format_obj(self, obj, alias):
result = u'%s* %-15s%s (%s - %s)' % (self.BOLD, '%s:' % obj.date, self.NC, self.temperature_display(obj.low), self.temperature_display(obj.high))
if hasattr(obj, 'text') and obj.text:
result += ' %s' % obj.text
return result
class CurrentFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'temp')
temperature_display = staticmethod(lambda t: u'%s' % t.value)
def format_obj(self, obj, alias):
result = u'%s%s%s: %s' % (self.BOLD, obj.date, self.NC, self.temperature_display(obj.temp))
if hasattr(obj, 'text') and obj.text:
result += u' - %s' % obj.text
return result
class CitiesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name')
def get_title(self, obj):
return obj.name
class WetBoobs(ReplApplication):
APPNAME = 'wetboobs'
VERSION = '1.4'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon'
DESCRIPTION = "Console application allowing to display weather and forecasts in your city."
SHORT_DESCRIPTION = "display weather and forecasts"
CAPS = CapWeather
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'cities': CitiesFormatter,
'current': CurrentFormatter,
'forecasts': ForecastsFormatter,
}
COMMANDS_FORMATTERS = {'cities': 'cities',
'current': 'current',
'forecasts': 'forecasts',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
@defaultcount(10)
def do_cities(self, pattern):
"""
cities PATTERN
Search cities.
"""
self.change_path(['cities'])
self.start_format()
for city in self.do('iter_city_search', pattern, caps=CapWeather):
self.cached_format(city)
def complete_current(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_current(self, line):
"""
current CITY_ID
Get current weather for specified city. Use the 'cities' command to find them.
"""
city, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(city)
tr = self.config.get('settings', 'temperature_display', default='C')
if tr == 'C':
self.formatter.temperature_display = lambda t: t.ascelsius()
elif tr == 'F':
self.formatter.temperature_display = lambda t: t.asfahrenheit()
self.start_format()
for current in self.do('get_current', _id, backends=backend_name, caps=CapWeather):
if current:
self.format(current)
def complete_forecasts(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_forecasts(self, line):
"""
forecasts CITY_ID
Get forecasts for specified city. Use the 'cities' command to find them.
"""
city, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(city)
tr = self.config.get('settings', 'temperature_display', default='C')
if tr == 'C':
self.formatter.temperature_display = lambda t: t.ascelsius()
elif tr == 'F':
self.formatter.temperature_display = lambda t: t.asfahrenheit()
self.start_format()
for forecast in self.do('iter_forecast', _id, backends=backend_name, caps=CapWeather):
self.format(forecast)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/ 0000775 0000000 0000000 00000000000 13204006733 0023260 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/__init__.py 0000664 0000000 0000000 00000002026 13204006733 0025371 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-2014 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .browsers import Browser, DomainBrowser, UrlNotAllowed, PagesBrowser, LoginBrowser, need_login, AbstractBrowser, StatesMixin
from .url import URL
__all__ = ['Browser', 'DomainBrowser', 'UrlNotAllowed', 'PagesBrowser', 'URL',
'LoginBrowser', 'need_login', 'AbstractBrowser', 'StatesMixin']
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/browsers.py 0000664 0000000 0000000 00000076360 13204006733 0025514 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-2014 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import absolute_import, print_function
from collections import OrderedDict
from functools import wraps
import re
import pickle
import base64
import zlib
from functools import reduce
try:
from requests.packages import urllib3
except ImportError:
import urllib3
import os
import sys
from copy import deepcopy
import inspect
from datetime import datetime, timedelta
from dateutil import parser
try:
import requests
if int(requests.__version__.split('.')[0]) < 2:
raise ImportError()
except ImportError:
raise ImportError('Please install python-requests >= 2.0')
from weboob.exceptions import BrowserHTTPSDowngrade, ModuleInstallError
from weboob.tools.log import getLogger
from weboob.tools.compat import basestring, unicode, urlparse, urljoin
from weboob.tools.json import json
from .cookies import WeboobCookieJar
from .exceptions import HTTPNotFound, ClientError, ServerError
from .sessions import FuturesSession
from .profiles import Firefox
from .pages import NextPage
from .url import URL, normalize_url
class Browser(object):
"""
Simple browser class.
Act like a browser, and don't try to do too much.
"""
PROFILE = Firefox()
"""
Default profile used by browser to navigate on websites.
"""
TIMEOUT = 10.0
"""
Default timeout during requests.
"""
REFRESH_MAX = 0.0
"""
When handling a Refresh header, the browsers considers it only if the sleep
time in lesser than this value.
"""
VERIFY = True
"""
Check SSL certificates.
"""
PROXIES = None
MAX_RETRIES = 2
MAX_WORKERS = 10
"""
Maximum of threads for asynchronous requests.
"""
ALLOW_REFERRER = True
"""
Controls the behavior of get_referrer.
"""
@classmethod
def asset(cls, localfile):
"""
Absolute file path for a module local file.
"""
if os.path.isabs(localfile):
return localfile
return os.path.join(os.path.dirname(inspect.getfile(cls)), localfile)
def __init__(self, logger=None, proxy=None, responses_dirname=None, weboob=None):
self.logger = getLogger('browser', logger)
self.responses_dirname = responses_dirname
self.responses_count = 1
if isinstance(self.VERIFY, basestring):
self.VERIFY = self.asset(self.VERIFY)
self.PROXIES = proxy
self._setup_session(self.PROFILE)
self.url = None
self.response = None
def deinit(self):
self.session.close()
def set_normalized_url(self, response, **kwargs):
response.url = normalize_url(response.url)
def save_response(self, response, warning=False, **kwargs):
if self.responses_dirname is None:
import tempfile
self.responses_dirname = tempfile.mkdtemp(prefix='weboob_session_')
print('Debug data will be saved in this directory: %s' % self.responses_dirname, file=sys.stderr)
elif not os.path.isdir(self.responses_dirname):
os.makedirs(self.responses_dirname)
import mimetypes
# get the content-type, remove optionnal charset part
mimetype = response.headers.get('Content-Type', '').split(';')[0]
# due to http://bugs.python.org/issue1043134
if mimetype == 'text/plain':
ext = '.txt'
else:
# try to get an extension (and avoid adding 'None')
ext = mimetypes.guess_extension(mimetype, False) or ''
path = re.sub(r'[^A-z0-9\.-_]+', '_', urlparse(response.url).path.rpartition('/')[2])[-10:]
if path.endswith(ext):
ext = ''
filename = '%02d-%d%s%s%s' % \
(self.responses_count, response.status_code, '-' if path else '', path, ext)
response_filepath = os.path.join(self.responses_dirname, filename)
request = response.request
with open(response_filepath + '-request.txt', 'w') as f:
f.write('%s %s\n\n\n' % (request.method, request.url))
for key, value in request.headers.items():
f.write('%s: %s\n' % (key, value))
if request.body is not None: # separate '' from None
f.write('\n\n\n%s' % request.body)
with open(response_filepath + '-response.txt', 'w') as f:
if hasattr(response.elapsed, 'total_seconds'):
f.write('Time: %3.3fs\n' % response.elapsed.total_seconds())
f.write('%s %s\n\n\n' % (response.status_code, response.reason))
for key, value in response.headers.items():
f.write('%s: %s\n' % (key, value))
with open(response_filepath, 'wb') as f:
f.write(response.content)
match_filepath = os.path.join(self.responses_dirname, 'url_response_match.txt')
with open(match_filepath, 'a') as f:
f.write('# %d %s %s\n' % (response.status_code, response.reason, response.headers.get('Content-Type', '')))
f.write('%s\t%s\n' % (response.url, filename))
self.responses_count += 1
msg = u'Response saved to %s' % response_filepath
if warning:
self.logger.warning(msg)
else:
self.logger.info(msg)
def _create_session(self):
return FuturesSession(max_workers=self.MAX_WORKERS, max_retries=self.MAX_RETRIES)
def _setup_session(self, profile):
"""
Set up a python-requests session for our usage.
"""
session = self._create_session()
session.proxies = self.PROXIES
session.verify = not self.logger.settings['ssl_insecure'] and self.VERIFY
if not session.verify:
try:
urllib3.disable_warnings()
except AttributeError:
# urllib3 is too old, warnings won't be disable
pass
# defines a max_retries. It's mandatory in case a server is not
# handling keep alive correctly, like the proxy burp
adapter_kwargs = dict(max_retries=self.MAX_RETRIES)
# set connection pool size equal to MAX_WORKERS if needed
if self.MAX_WORKERS > requests.adapters.DEFAULT_POOLSIZE:
adapter_kwargs.update(pool_connections=self.MAX_WORKERS,
pool_maxsize=self.MAX_WORKERS)
session.mount('https://', requests.adapters.HTTPAdapter(**adapter_kwargs))
session.mount('http://', requests.adapters.HTTPAdapter(**adapter_kwargs))
if self.TIMEOUT:
session.timeout = self.TIMEOUT
## weboob only can provide proxy and HTTP auth options
session.trust_env = False
profile.setup_session(session)
session.hooks['response'].append(self.set_normalized_url)
if self.responses_dirname is not None:
session.hooks['response'].append(self.save_response)
self.session = session
session.cookies = WeboobCookieJar()
def set_profile(self, profile):
profile.setup_session(self.session)
def location(self, url, **kwargs):
"""
Like :meth:`open` but also changes the current URL and response.
This is the most common method to request web pages.
Other than that, has the exact same behavior of open().
"""
assert not kwargs.get('is_async'), "Please use open() instead of location() to make asynchronous requests."
response = self.open(url, **kwargs)
self.response = response
self.url = self.response.url
return response
def open(self, url, referrer=None,
allow_redirects=True,
stream=None,
timeout=None,
verify=None,
cert=None,
proxies=None,
data_encoding=None,
is_async=False,
callback=lambda response: response,
**kwargs):
"""
Make an HTTP request like a browser does:
* follow redirects (unless disabled)
* provide referrers (unless disabled)
Unless a `method` is explicitly provided, it makes a GET request,
or a POST if data is not None,
An empty `data` (not None, like '' or {}) *will* make a POST.
It is a wrapper around session.request().
All session.request() options are available.
You should use location() or open() and not session.request(),
since it has some interesting additions, which are easily
individually disabled through the arguments.
Call this instead of location() if you do not want to "visit" the URL
(for instance, you are downloading a file).
When `is_async` is True, open() returns a Future object (see
concurrent.futures for more details), which can be evaluated with its
result() method. If any exception is raised while processing request,
it is caught and re-raised when calling result().
For example:
>>> Browser().open('http://google.com', is_async=True).result().text # doctest: +SKIP
:param url: URL
:type url: str
:param data: POST data
:type url: str or dict or None
:param referrer: Force referrer. False to disable sending it, None for guessing
:type referrer: str or False or None
:param is_async: Process request in a non-blocking way
:type is_async: bool
:param callback: Callback to be called when request has finished,
with response as its first and only argument
:type callback: function
:rtype: :class:`requests.Response`
"""
if 'async' in kwargs:
import warnings
warnings.warn('Please use is_async instead of async.', DeprecationWarning)
is_async = kwargs['async']
del kwargs['async']
if isinstance(url, basestring):
url = normalize_url(url)
elif isinstance(url, requests.Request):
url.url = normalize_url(url.url)
req = self.build_request(url, referrer, data_encoding=data_encoding, **kwargs)
preq = self.prepare_request(req)
if hasattr(preq, '_cookies'):
# The _cookies attribute is not present in requests < 2.2. As in
# previous version it doesn't calls extract_cookies_to_jar(), it is
# not a problem as we keep our own cookiejar instance.
preq._cookies = WeboobCookieJar.from_cookiejar(preq._cookies)
if proxies is None:
proxies = self.PROXIES
if verify is None:
verify = not self.logger.settings['ssl_insecure'] and self.VERIFY
if timeout is None:
timeout = self.TIMEOUT
# We define an inner_callback here in order to execute the same code
# regardless of is_async param.
def inner_callback(future, response):
if allow_redirects:
response = self.handle_refresh(response)
self.raise_for_status(response)
return callback(response)
# call python-requests
response = self.session.send(preq,
allow_redirects=allow_redirects,
stream=stream,
timeout=timeout,
verify=verify,
cert=cert,
proxies=proxies,
callback=inner_callback,
is_async=is_async)
return response
def async_open(self, url, **kwargs):
"""
Shortcut to open(url, is_async=True).
"""
if 'async' in kwargs:
del kwargs['async']
if 'is_async' in kwargs:
del kwargs['is_async']
return self.open(url, is_async=True, **kwargs)
def raise_for_status(self, response):
"""
Like Response.raise_for_status but will use other classes if needed.
"""
http_error_msg = None
if 400 <= response.status_code < 500:
http_error_msg = '%s Client Error: %s' % (response.status_code, response.reason)
cls = ClientError
if response.status_code == 404:
cls = HTTPNotFound
elif 500 <= response.status_code < 600:
http_error_msg = '%s Server Error: %s' % (response.status_code, response.reason)
cls = ServerError
if http_error_msg:
raise cls(http_error_msg, response=response)
# in case we did not catch something that should be
response.raise_for_status()
def build_request(self, url, referrer=None, data_encoding=None, **kwargs):
"""
Does the same job as open(), but returns a Request without
submitting it.
This allows further customization to the Request.
"""
if isinstance(url, requests.Request):
req = url
url = req.url
else:
req = requests.Request(url=url, **kwargs)
# guess method
if req.method is None:
if req.data:
req.method = 'POST'
else:
req.method = 'GET'
# convert unicode strings to proper encoding
if isinstance(req.data, unicode) and data_encoding:
req.data = req.data.encode(data_encoding)
if isinstance(req.data, dict) and data_encoding:
req.data = OrderedDict([(k, v.encode(data_encoding) if isinstance(v, unicode) else v)
for k, v in req.data.items()])
if referrer is None:
referrer = self.get_referrer(self.url, url)
if referrer:
# Yes, it is a misspelling.
req.headers.setdefault('Referer', referrer)
return req
def prepare_request(self, req):
"""
Get a prepared request from a Request object.
This method aims to be overloaded by children classes.
"""
return self.session.prepare_request(req)
REFRESH_RE = re.compile(r"^(?P[\d\.]+)(;\s*url=[\"']?(?P.*?)[\"']?)?$", re.IGNORECASE)
def handle_refresh(self, response):
"""
Called by open, to handle Refresh HTTP header.
It only redirect to the refresh URL if the sleep time is inferior to
REFRESH_MAX.
"""
if 'Refresh' not in response.headers:
return response
m = self.REFRESH_RE.match(response.headers['Refresh'])
if m:
# XXX perhaps we should not redirect if the refresh url is equal to the current url.
url = m.groupdict().get('url', None) or response.request.url
sleep = float(m.groupdict()['sleep'])
if sleep <= self.REFRESH_MAX:
self.logger.debug('Refresh to %s' % url)
return self.open(url)
else:
self.logger.debug('Do not refresh to %s because %s > REFRESH_MAX(%s)' % (url, sleep, self.REFRESH_MAX))
return response
self.logger.warning('Unable to handle refresh "%s"' % response.headers['Refresh'])
return response
def get_referrer(self, oldurl, newurl):
"""
Get the referrer to send when doing a request.
If we should not send a referrer, it will return None.
Reference: https://en.wikipedia.org/wiki/HTTP_referer
The behavior can be controlled through the ALLOW_REFERRER attribute.
True always allows the referers
to be sent, False never, and None only if it is within
the same domain.
:param oldurl: Current absolute URL
:type oldurl: str or None
:param newurl: Target absolute URL
:type newurl: str
:rtype: str or None
"""
if self.ALLOW_REFERRER is False:
return
if oldurl is None:
return
old = urlparse(oldurl)
new = urlparse(newurl)
# Do not leak secure URLs to insecure URLs
if old.scheme == 'https' and new.scheme != 'https':
return
# Reloading the page. Usually no referrer.
if oldurl == newurl:
return
# Domain-based privacy
if self.ALLOW_REFERRER is None and old.netloc != new.netloc:
return
return oldurl
def export_session(self):
def make_cookie(c):
d = {
k: getattr(c, k) for k in ['name', 'value', 'domain', 'path', 'secure']
}
#d['session'] = c.discard
d['httpOnly'] = 'httponly' in [k.lower() for k in c._rest.keys()]
d['expirationDate'] = getattr(c, 'expires', None)
return d
return {
'url': self.url,
'cookies': [make_cookie(c) for c in self.session.cookies],
}
class UrlNotAllowed(Exception):
"""
Raises by :class:`DomainBrowser` when `RESTRICT_URL` is set and trying to go
on an url not matching `BASEURL`.
"""
class DomainBrowser(Browser):
"""
A browser that handles relative URLs and can have a base URL (usually a domain).
For instance self.location('/hello') will get http://weboob.org/hello
if BASEURL is 'http://weboob.org/'.
"""
BASEURL = None
"""
Base URL, e.g. 'http://weboob.org/' or 'https://weboob.org/'
See absurl().
"""
RESTRICT_URL = False
"""
URLs allowed to load.
This can be used to force SSL (if the BASEURL is SSL) or any other leakage.
Set to True to allow only URLs starting by the BASEURL.
Set it to a list of allowed URLs if you have multiple allowed URLs.
More complex behavior is possible by overloading url_allowed()
"""
def __init__(self, baseurl=None, *args, **kwargs):
super(DomainBrowser, self).__init__(*args, **kwargs)
if baseurl is not None:
self.BASEURL = baseurl
def url_allowed(self, url):
"""
Checks if we are allowed to visit an URL.
See RESTRICT_URL.
:param url: Absolute URL
:type url: str
:rtype: bool
"""
if self.BASEURL is None or self.RESTRICT_URL is False:
return True
if self.RESTRICT_URL is True:
return url.startswith(self.BASEURL)
for restrict_url in self.RESTRICT_URL:
if url.startswith(restrict_url):
return True
return False
def absurl(self, uri, base=None):
"""
Get the absolute URL, relative to a base URL.
If base is None, it will try to use the current URL.
If there is no current URL, it will try to use BASEURL.
If base is False, it will always try to use the current URL.
If base is True, it will always try to use BASEURL.
:param uri: URI to make absolute. It can be already absolute.
:type uri: str
:param base: Base absolute URL.
:type base: str or None or False or True
:rtype: str
"""
if not base:
base = self.url
if base is None or base is True:
base = self.BASEURL
return urljoin(base, uri)
def open(self, req, *args, **kwargs):
"""
Like :meth:`Browser.open` but handles urls without domains, using
the :attr:`BASEURL` attribute.
"""
uri = req.url if isinstance(req, requests.Request) else req
url = self.absurl(uri)
if not self.url_allowed(url):
raise UrlNotAllowed(url)
if isinstance(req, requests.Request):
req.url = url
else:
req = url
return super(DomainBrowser, self).open(req, *args, **kwargs)
def go_home(self):
"""
Go to the "home" page, usually the BASEURL.
"""
return self.location(self.BASEURL or self.absurl('/'))
class PagesBrowser(DomainBrowser):
r"""
A browser which works pages and keep state of navigation.
To use it, you have to derive it and to create URL objects as class
attributes. When open() or location() are called, if the url matches
one of URL objects, it returns a Page object. In case of location(), it
stores it in self.page.
Example:
>>> from .pages import HTMLPage
>>> class HomePage(HTMLPage):
... pass
...
>>> class ListPage(HTMLPage):
... pass
...
>>> class MyBrowser(PagesBrowser):
... BASEURL = 'http://example.org'
... home = URL('/(index\.html)?', HomePage)
... list = URL('/list\.html', ListPage)
...
You can then use URL instances to go on pages.
"""
_urls = None
def __init__(self, *args, **kwargs):
self.highlight_el = kwargs.pop('highlight_el', False)
super(PagesBrowser, self).__init__(*args, **kwargs)
self.page = None
# exclude properties because they can access other fields not yet defined
def is_property(attr):
v = getattr(type(self), attr, None)
return hasattr(v, '__get__') or hasattr(v, '__set__')
attrs = [(attr, getattr(self, attr)) for attr in dir(self) if not is_property(attr)]
attrs = [v for v in attrs if isinstance(v[1], URL)]
attrs.sort(key=lambda v: v[1]._creation_counter)
self._urls = OrderedDict(deepcopy(attrs))
for k, v in self._urls.items():
setattr(self, k, v)
for url in self._urls.values():
url.browser = self
def open(self, *args, **kwargs):
"""
Same method than
:meth:`weboob.browser.browsers.DomainBrowser.open`, but the
response contains an attribute `page` if the url matches any
:class:`URL` object.
"""
callback = kwargs.pop('callback', lambda response: response)
# Have to define a callback to seamlessly process synchronous and
# asynchronous requests, see :meth:`Browser.open` and its `is_async`
# and `callback` params.
def internal_callback(response):
# Try to handle the response page with an URL instance.
response.page = None
for url in self._urls.values():
page = url.handle(response)
if page is not None:
self.logger.debug('Handle %s with %s' % (response.url, page.__class__.__name__))
response.page = page
break
if response.page is None:
regexp = r'^(?P\w+)://.*'
proto_response = re.match(regexp, response.url)
if proto_response:
proto_response = proto_response.group('proto')
proto_base = re.match(regexp, self.BASEURL).group('proto')
if proto_base == 'https' and proto_response != 'https':
raise BrowserHTTPSDowngrade()
self.logger.debug('Unable to handle %s' % response.url)
return callback(response)
return super(PagesBrowser, self).open(callback=internal_callback, *args, **kwargs)
def location(self, *args, **kwargs):
"""
Same method than
:meth:`weboob.browser.browsers.Browser.location`, but if the
url matches any :class:`URL` object, an attribute `page` is added to
response, and the attribute :attr:`PagesBrowser.page` is set.
"""
if self.page is not None:
# Call leave hook.
self.page.on_leave()
response = self.open(*args, **kwargs)
self.response = response
self.page = response.page
self.url = response.url
if self.page is not None:
# Call load hook.
self.page.on_load()
# Returns self.response in case on_load recalls location()
return self.response
def pagination(self, func, *args, **kwargs):
r"""
This helper function can be used to handle pagination pages easily.
When the called function raises an exception :class:`NextPage`, it goes
on the wanted page and recall the function.
:class:`NextPage` constructor can take an url or a Request object.
>>> from .pages import HTMLPage
>>> class Page(HTMLPage):
... def iter_values(self):
... for el in self.doc.xpath('//li'):
... yield el.text
... for next in self.doc.xpath('//a'):
... raise NextPage(next.attrib['href'])
...
>>> class Browser(PagesBrowser):
... BASEURL = 'https://people.symlink.me'
... list = URL('/~rom1/projects/weboob/list-(?P\d+).html', Page)
...
>>> b = Browser()
>>> b.list.go(pagenum=1) # doctest: +ELLIPSIS
>>> list(b.pagination(lambda: b.page.iter_values()))
['One', 'Two', 'Three', 'Four']
"""
while True:
try:
for r in func(*args, **kwargs):
yield r
except NextPage as e:
self.location(e.request)
else:
return
def need_login(func):
"""
Decorator used to require to be logged to access to this function.
"""
@wraps(func)
def inner(browser, *args, **kwargs):
if (not hasattr(browser, 'logged') or (hasattr(browser, 'logged') and not browser.logged)) and \
(not hasattr(browser, 'page') or browser.page is None or not browser.page.logged):
browser.do_login()
if browser.logger.settings.get('export_session'):
browser.logger.debug('logged in with session: %s', json.dumps(browser.export_session()))
return func(browser, *args, **kwargs)
return inner
class LoginBrowser(PagesBrowser):
"""
A browser which supports login.
"""
def __init__(self, username, password, *args, **kwargs):
super(LoginBrowser, self).__init__(*args, **kwargs)
self.username = username
self.password = password
def do_login(self):
"""
Abstract method to implement to login on website.
It is call when a login is needed.
"""
raise NotImplementedError()
def do_logout(self):
"""
Logout from website.
By default, simply clears the cookies.
"""
self.session.cookies.clear()
class StatesMixin(object):
"""
Mixin to store states of browser.
"""
__states__ = []
"""
Saved state variables.
"""
STATE_DURATION = None
"""
In minutes, used to set an expiration datetime object of the state.
"""
def locate_browser(self, state):
try:
self.location(state['url'])
except (requests.exceptions.HTTPError, requests.exceptions.TooManyRedirects):
pass
def load_state(self, state):
if 'expire' in state and parser.parse(state['expire']) < datetime.now():
return self.logger.info('State expired, not reloading it from storage')
if 'cookies' in state:
try:
self.session.cookies = pickle.loads(zlib.decompress(base64.b64decode(state['cookies'])))
except (TypeError, zlib.error, EOFError, ValueError):
self.logger.error('Unable to reload cookies from storage')
else:
self.logger.info('Reloaded cookies from storage')
for attrname in self.__states__:
if attrname in state:
setattr(self, attrname, state[attrname])
if 'url' in state:
self.locate_browser(state)
def dump_state(self):
state = {}
if hasattr(self, 'page') and self.page:
state['url'] = self.page.url
state['cookies'] = base64.b64encode(zlib.compress(pickle.dumps(self.session.cookies, -1)))
for attrname in self.__states__:
try:
state[attrname] = getattr(self, attrname)
except AttributeError:
pass
if self.STATE_DURATION is not None:
state['expire'] = unicode((datetime.now() + timedelta(minutes=self.STATE_DURATION)).replace(microsecond=0))
self.logger.info('Stored cookies into storage')
return state
class APIBrowser(DomainBrowser):
"""
A browser for API websites.
"""
def build_request(self, *args, **kwargs):
if 'data' in kwargs:
kwargs['data'] = json.dumps(kwargs['data'])
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Content-Type'] = 'application/json'
return super(APIBrowser, self).build_request(*args, **kwargs)
def open(self, *args, **kwargs):
"""
Do a JSON request.
The "Content-Type" header is always set to "application/json".
:param data: if specified, format as JSON and send as request body
:type data: :class:`dict`
:param headers: if specified, add these headers to the request
:type headers: :class:`dict`
"""
return super(APIBrowser, self).open(*args, **kwargs)
def request(self, *args, **kwargs):
"""
Do a JSON request and parse the response.
:returns: a dict containing the parsed JSON server response
:rtype: :class:`dict`
"""
return self.open(*args, **kwargs).json()
class AbstractBrowserMissingParentError(Exception):
pass
class AbstractBrowser(Browser):
""" AbstractBrowser allow inheritance of a browser defined in another module.
Websites can share many pages and code base. This class allow to load a browser
provided by another module and to build our own browser on top of it (like standard
python inheritance. Weboob will install and download the PARENT module for you.
PARENT is a mandatory attribute, it's the name of the module providing the parent Browser
PARENT_ATTR is an optionnal attribute used when the parent module does not have only one
browser defined as BROWSER class attribute: you can customized the path of the object to load.
Note that you must pass a valid weboob instance as first argument of the constructor.
"""
PARENT = None
PARENT_ATTR = None
def __new__(cls, *args, **kwargs):
weboob = kwargs['weboob']
if cls.PARENT is None:
raise AbstractBrowserMissingParentError("PARENT is not defined for browser %s" % cls)
try:
module = weboob.load_or_install_module(cls.PARENT)
except ModuleInstallError as err:
raise ModuleInstallError('This module depends on %s module but %s\'s installation failed with: %s' % (cls.PARENT, cls.PARENT, err))
if cls.PARENT_ATTR is None:
parent = module.klass.BROWSER
else:
parent = reduce(getattr, cls.PARENT_ATTR.split('.'), module)
if parent is None:
raise AbstractBrowserMissingParentError("Failed to load parent class")
cls.__bases__ = (parent,)
return object.__new__(cls)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/cache.py 0000664 0000000 0000000 00000006637 13204006733 0024711 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-2016 weboob project
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
__all__ = ['CacheMixin']
class CacheEntry(object):
def __init__(self, response):
self.response = response
self.etag = response.headers.get('ETag')
self.last_modified = response.headers.get('Last-Modified')
def has_cache_key(self):
return (self.etag or self.last_modified)
def update_request(self, request):
if self.last_modified:
request.headers['If-Modified-Since'] = self.last_modified
if self.etag:
request.headers['If-None-Match'] = self.etag
class CacheMixin(object):
"""Mixin to inherit in a Browser"""
def __init__(self, *args, **kwargs):
super(CacheMixin, self).__init__(*args, **kwargs)
self.cache = {}
"""Cache store object
To limit the size of the cache, a :class:`weboob.tools.lrudict.LimitedLRUDict`
instance can be used.
"""
self.is_updatable = True
"""Whether the cache is updatable
If `False`, once a request has been successfully executed, its response
will always be returned.
If `True`, the `ETag` and `Last-Modified` of the response will be
stored along with the cache. When the request is re-executed, instead
of simply returning the previous response, the server is queried to
check if a newer version of the page exists.
If a newer page exists, it is returned instead and overwrites the
obsolete page in the cache.
"""
def make_cache_key(self, request):
"""Make a key for the cache corresponding to the request."""
body = getattr(request, 'body', None)
headers = tuple(request.headers.values())
return (request.method, request.url, body, headers)
def open_with_cache(self, url, **kwargs):
"""Perform a request using the cache if possible."""
request = self.build_request(url, **kwargs)
key = self.make_cache_key(request)
if key in self.cache:
if not self.is_updatable:
self.logger.debug('cache HIT for %r', request.url)
return self.cache[key].response
else:
self.cache[key].update_request(request)
response = super(CacheMixin, self).open(request, **kwargs)
if response.status_code == 304:
self.logger.debug('cache HIT for %r', request.url)
return self.cache[key].response
elif response.status_code == 200:
entry = CacheEntry(response)
if entry.has_cache_key():
self.logger.debug('storing %r response in cache', request.url)
self.cache[key] = entry
self.logger.debug('cache MISS for %r', request.url)
return response
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/cookies.py 0000664 0000000 0000000 00000003032 13204006733 0025264 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2014 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests.cookies
try:
import cookielib
except ImportError:
import http.cookiejar as cookielib
__all__ = ['WeboobCookieJar']
class WeboobCookieJar(requests.cookies.RequestsCookieJar):
@classmethod
def from_cookiejar(klass, cj):
"""
Create a WeboobCookieJar from another CookieJar instance.
"""
return requests.cookies.merge_cookies(klass(), cj)
def export(self, filename):
"""
Export all cookies to a file, regardless of expiration, etc.
"""
cj = requests.cookies.merge_cookies(cookielib.LWPCookieJar(), self)
cj.save(filename, ignore_discard=True, ignore_expires=True)
def copy(self):
"""Return an object copy of the cookie jar."""
new_cj = type(self)()
new_cj.update(self)
return new_cj
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/elements.py 0000664 0000000 0000000 00000030751 13204006733 0025454 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import re
import sys
from collections import OrderedDict
from copy import deepcopy
import lxml.html
from weboob.tools.log import getLogger, DEBUG_FILTERS
from weboob.tools.compat import basestring, unicode, with_metaclass
from weboob.browser.pages import NextPage
from .filters.standard import _Filter, CleanText
from .filters.html import AttributeNotFound, XPathNotFound
__all__ = ['DataError', 'AbstractElement', 'ListElement', 'ItemElement', 'TableElement', 'SkipItem']
class DataError(Exception):
"""
Returned data from pages are incoherent.
"""
def method(klass):
"""
Class-decorator to call it as a method.
"""
def inner(self, *args, **kwargs):
return klass(self)(*args, **kwargs)
return inner
class AbstractElement(object):
_creation_counter = 0
condition = None
def __init__(self, page, parent=None, el=None):
self.page = page
self.parent = parent
if el is not None:
self.el = el
elif parent is not None:
self.el = parent.el
else:
self.el = page.doc
if parent is not None:
self.env = deepcopy(parent.env)
else:
self.env = deepcopy(page.params)
# Used by debug
self._random_id = AbstractElement._creation_counter
AbstractElement._creation_counter += 1
self.loaders = {}
def use_selector(self, func, key=None):
if isinstance(func, _Filter):
func._obj = self
func._key = key
value = func(self)
elif isinstance(func, type) and issubclass(func, ItemElement):
value = func(self.page, self, self.el)()
elif callable(func):
value = func()
else:
value = deepcopy(func)
return value
def parse(self, obj):
pass
def cssselect(self, *args, **kwargs):
return self.el.cssselect(*args, **kwargs)
def xpath(self, *args, **kwargs):
return self.el.xpath(*args, **kwargs)
def handle_loaders(self):
for attrname in dir(self):
m = re.match('load_(.*)', attrname)
if not m:
continue
name = m.group(1)
if name in self.loaders:
continue
loader = getattr(self, attrname)
self.loaders[name] = self.use_selector(loader, key=attrname)
class ListElement(AbstractElement):
item_xpath = None
flush_at_end = False
ignore_duplicate = False
def __init__(self, *args, **kwargs):
super(ListElement, self).__init__(*args, **kwargs)
self.logger = getLogger(self.__class__.__name__.lower())
self.objects = OrderedDict()
def __call__(self, *args, **kwargs):
for key, value in kwargs.items():
self.env[key] = value
return self.__iter__()
def find_elements(self):
"""
Get the nodes that will have to be processed.
This method can be overridden if xpath filters are not
sufficient.
"""
if self.item_xpath is not None:
for el in self.el.xpath(self.item_xpath):
yield el
else:
yield self.el
def __iter__(self):
if self.condition is not None and not self.condition():
return
self.parse(self.el)
items = []
for el in self.find_elements():
for attrname in dir(self):
attr = getattr(self, attrname)
if isinstance(attr, type) and issubclass(attr, AbstractElement) and attr != type(self):
item = attr(self.page, self, el)
if item.condition is not None and not item.condition():
continue
item.handle_loaders()
items.append(item)
for item in items:
for obj in item:
obj = self.store(obj)
if obj and not self.flush_at_end:
yield obj
if self.flush_at_end:
for obj in self.flush():
yield obj
self.check_next_page()
def flush(self):
for obj in self.objects.values():
yield obj
def check_next_page(self):
if not hasattr(self, 'next_page'):
return
next_page = getattr(self, 'next_page')
try:
value = self.use_selector(next_page)
except (AttributeNotFound, XPathNotFound):
return
if value is None:
return
raise NextPage(value)
def store(self, obj):
if obj.id:
if obj.id in self.objects:
if self.ignore_duplicate:
self.logger.warning('There are two objects with the same ID! %s' % obj.id)
return
else:
raise DataError('There are two objects with the same ID! %s' % obj.id)
self.objects[obj.id] = obj
return obj
class SkipItem(Exception):
"""
Raise this exception in an :class:`ItemElement` subclass to skip an item.
"""
class _ItemElementMeta(type):
"""
Private meta-class used to keep order of obj_* attributes in :class:`ItemElement`.
"""
def __new__(mcs, name, bases, attrs):
_attrs = []
for base in bases:
if hasattr(base, '_attrs'):
_attrs += base._attrs
filters = [(re.sub('^obj_', '', attr_name), attrs[attr_name]) for attr_name, obj in attrs.items() if attr_name.startswith('obj_')]
# constants first, then filters, then methods
filters.sort(key=lambda x: x[1]._creation_counter if hasattr(x[1], '_creation_counter') else (sys.maxsize if callable(x[1]) else 0))
new_class = super(_ItemElementMeta, mcs).__new__(mcs, name, bases, attrs)
new_class._attrs = _attrs + [f[0] for f in filters]
return new_class
class ItemElement(with_metaclass(_ItemElementMeta, AbstractElement)):
_attrs = None
_loaders = None
klass = None
validate = None
class Index(object):
pass
def __init__(self, *args, **kwargs):
super(ItemElement, self).__init__(*args, **kwargs)
self.logger = getLogger(self.__class__.__name__.lower())
self.obj = None
self.saved_attrib = {} # safer way would be to clone lxml tree
def build_object(self):
if self.klass is None:
return
return self.klass()
def _restore_attrib(self):
for el in self.saved_attrib:
el.attrib.clear()
el.attrib.update(self.saved_attrib[el])
self.saved_attrib = {}
def should_highlight(self):
try:
responses_dirname = self.page.browser.responses_dirname and self.page.browser.highlight_el
if not responses_dirname:
return False
if not self.el.getroottree():
return False
except AttributeError:
return False
else:
return True
def _write_highlighted(self):
if not self.should_highlight():
return
responses_dirname = self.page.browser.responses_dirname
html = lxml.html.tostring(self.el.getroottree().getroot())
fn = os.path.join(responses_dirname, 'obj-%s.html' % self._random_id)
with open(fn, 'w') as fd:
fd.write(html)
self.logger.debug('highlighted object to %s', fn)
def __call__(self, obj=None):
if obj is not None:
self.obj = obj
for obj in self:
return obj
def __iter__(self):
if self.condition is not None and not self.condition():
return
highlight = False
try:
if self.should_highlight():
self.saved_attrib[self.el] = dict(self.el.attrib)
self.el.attrib['style'] = 'color: white !important; background: orange !important;'
try:
if self.obj is None:
self.obj = self.build_object()
self.parse(self.el)
self.handle_loaders()
for attr in self._attrs:
self.handle_attr(attr, getattr(self, 'obj_%s' % attr))
except SkipItem:
return
if self.validate is not None and not self.validate(self.obj):
return
highlight = True
finally:
if highlight:
self._write_highlighted()
self._restore_attrib()
yield self.obj
def handle_attr(self, key, func):
try:
value = self.use_selector(func, key=key)
except SkipItem as e:
# Help debugging as tracebacks do not give us the key
self.logger.debug("Attribute %s raises a %r", key, e)
raise
except Exception as e:
# If we are here, we have probably a real parsing issue
self.logger.warning('Attribute %s raises %s', key, repr(e))
raise
logger = getLogger('b2filters')
logger.log(DEBUG_FILTERS, "%s.%s = %r" % (self._random_id, key, value))
setattr(self.obj, key, value)
class TableElement(ListElement):
head_xpath = None
cleaner = CleanText
def __init__(self, *args, **kwargs):
super(TableElement, self).__init__(*args, **kwargs)
self._cols = {}
columns = {}
for attrname in dir(self):
m = re.match('col_(.*)', attrname)
if m:
cols = getattr(self, attrname)
if not isinstance(cols, (list,tuple)):
cols = [cols]
columns[m.group(1)] = [s.lower() if isinstance(s, (str, unicode)) else s for s in cols]
colnum = 0
for el in self.el.xpath(self.head_xpath):
title = self.cleaner.clean(el)
for name, titles in columns.items():
if name in self._cols:
continue
if title.lower() in [s for s in titles if isinstance(s, (str, unicode))] or \
any(map(lambda x: x.match(title), [s for s in titles if isinstance(s, type(re.compile('')))])):
self._cols[name] = colnum
try:
colnum += int(el.attrib.get('colspan', 1))
except (ValueError, AttributeError):
colnum += 1
def get_colnum(self, name):
return self._cols.get(name, None)
class DictElement(ListElement):
def find_elements(self):
if self.item_xpath is None:
selector = []
elif isinstance(self.item_xpath, basestring):
selector = self.item_xpath.split('/')
else:
selector = self.item_xpath
bases = [self.el]
for key in selector:
if key == '*':
bases = sum([el if isinstance(el, list) else list(el.values()) for el in bases], [])
else:
bases = [el[int(key)] if isinstance(el, list) else el[key] for el in bases]
for base in bases:
for el in base:
yield el
def magic_highlight(els, open_browser=True):
"""Open a web browser with the document open and the element highlighted"""
import lxml.html
import webbrowser
import tempfile
if not els:
raise Exception('no elements to highlight')
if not isinstance(els, (list, tuple)):
els = [els]
saved = {}
for el in els:
saved[el] = el.attrib.get('style', '')
el.attrib['style'] = 'color: white !important; background: red !important;'
html = lxml.html.tostring(el.xpath('/*')[0])
for el in els:
el.attrib['style'] = saved[el]
_, fn = tempfile.mkstemp(dir='/tmp', prefix='weboob-highlight', suffix='.html')
with open(fn, 'w') as fd:
fd.write(html)
print('Saved to %r' % fn)
if open_browser:
webbrowser.open('file://%s' % fn)
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/exceptions.py 0000664 0000000 0000000 00000002001 13204006733 0026004 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2014 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from requests.exceptions import HTTPError
from weboob.exceptions import BrowserHTTPError, BrowserHTTPNotFound
class HTTPNotFound(HTTPError, BrowserHTTPNotFound):
pass
class ClientError(HTTPError, BrowserHTTPError):
pass
class ServerError(HTTPError, BrowserHTTPError):
pass
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/filters/ 0000775 0000000 0000000 00000000000 13204006733 0024730 5 ustar 00root root 0000000 0000000 woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/filters/__init__.py 0000664 0000000 0000000 00000001331 13204006733 0027037 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/filters/base.py 0000664 0000000 0000000 00000013531 13204006733 0026217 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2017 weboob project
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from functools import wraps
import lxml.html
from weboob.exceptions import ParseError
from weboob.tools.compat import unicode, basestring
from weboob.tools.log import getLogger, DEBUG_FILTERS
__all__ = ['FilterError', 'Filter',]
class NoDefault(object):
def __repr__(self):
return 'NO_DEFAULT'
_NO_DEFAULT = NoDefault()
class FilterError(ParseError):
pass
class _Filter(object):
_creation_counter = 0
def __init__(self, default=_NO_DEFAULT):
self._key = None
self._obj = None
self.default = default
self._creation_counter = _Filter._creation_counter
_Filter._creation_counter += 1
def __or__(self, o):
self.default = o
return self
def __and__(self, o):
if isinstance(o, type) and issubclass(o, _Filter):
o = o()
o.selector = self
return o
def default_or_raise(self, exception):
if self.default is not _NO_DEFAULT:
return self.default
else:
raise exception
def __str__(self):
return self.__class__.__name__
def highlight_el(self, el, item=None):
obj = self._obj or item
try:
if not hasattr(obj, 'saved_attrib'):
return
if not obj.page.browser.highlight_el:
return
except AttributeError:
return
if el not in obj.saved_attrib:
obj.saved_attrib[el] = dict(el.attrib)
el.attrib['style'] = 'color: white !important; background: red !important;'
if self._key:
el.attrib['title'] = 'weboob field: %s' % self._key
def debug(*args):
"""
A decorator function to provide some debug information
in Filters.
It prints by default the name of the Filter and the input value.
"""
def wraper(function):
@wraps(function)
def print_debug(self, value):
logger = getLogger('b2filters')
result = ''
outputvalue = value
if isinstance(value, list):
from lxml import etree
outputvalue = ''
first = True
for element in value:
if first:
first = False
else:
outputvalue += ', '
if isinstance(element, etree.ElementBase):
outputvalue += "%s" % etree.tostring(element, encoding=unicode)
else:
outputvalue += "%r" % element
if self._obj is not None:
result += "%s" % self._obj._random_id
if self._key is not None:
result += ".%s" % self._key
name = str(self)
result += " %s(%r" % (name, outputvalue)
for arg in self.__dict__:
if arg.startswith('_') or arg == u"selector":
continue
if arg == u'default' and getattr(self, arg) == _NO_DEFAULT:
continue
result += ", %s=%r" % (arg, getattr(self, arg))
result += u')'
logger.log(DEBUG_FILTERS, result)
res = function(self, value)
return res
return print_debug
return wraper
class Filter(_Filter):
"""
Class used to filter on a HTML element given as call parameter to return
matching elements.
Filters can be chained, so the parameter supplied to constructor can be
either a xpath selector string, or an other filter called before.
>>> from lxml.html import etree
>>> f = CleanDecimal(CleanText('//p'), replace_dots=True)
>>> f(etree.fromstring('
blah: 229,90
'))
Decimal('229.90')
"""
def __init__(self, selector=None, default=_NO_DEFAULT):
"""
:param default: default value in case the filter fails to find or parse
the requested value
"""
super(Filter, self).__init__(default=default)
self.selector = selector
def select(self, selector, item):
if isinstance(selector, basestring):
ret = item.xpath(selector)
elif isinstance(selector, _Filter):
selector._key = self._key
selector._obj = self._obj
ret = selector(item)
elif callable(selector):
ret = selector(item)
else:
ret = selector
if isinstance(ret, lxml.html.HtmlElement):
self.highlight_el(ret, item)
elif isinstance(ret, list):
for el in ret:
if isinstance(el, lxml.html.HtmlElement):
self.highlight_el(el, item)
return ret
def __call__(self, item):
return self.filter(self.select(self.selector, item))
@debug()
def filter(self, value):
"""
This method has to be overridden by children classes.
"""
raise NotImplementedError()
class _Selector(Filter):
def filter(self, elements):
if elements is not None:
return elements
else:
return self.default_or_raise(ParseError('Element %r not found' % self.selector))
woob-489c25825d76436efb4ea31102bc559543428899-weboob/weboob/browser/filters/html.py 0000664 0000000 0000000 00000014553 13204006733 0026256 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import lxml.html as html
from weboob.exceptions import ParseError
from weboob.tools.compat import basestring, unicode, urljoin
from weboob.tools.html import html2text
from .base import _NO_DEFAULT, Filter, FilterError, _Selector, debug
from .standard import TableCell # TODO move class here when modules are migrated
__all__ = ['CSS', 'XPath', 'XPathNotFound', 'AttributeNotFound',
'Attr', 'Link', 'AbsoluteLink',
'CleanHTML', 'FormValue', 'HasElement',
'TableCell',
]
class XPathNotFound(FilterError):
pass
class AttributeNotFound(FilterError):
pass
class CSS(_Selector):
"""Select HTML elements with a CSS selector
For example::
obj_foo = CleanText(CSS('div.main'))
will take the text of all ``