aboutsummaryrefslogtreecommitdiffstats
path: root/cli
diff options
context:
space:
mode:
authorAaron LI <aly@aaronly.me>2017-11-03 18:45:20 +0800
committerAaron LI <aly@aaronly.me>2017-11-03 18:45:20 +0800
commit5a8b44ee3328464a673c2be2e21873fb02f1425b (patch)
tree192c3cc96018a26a85ec470aad1f814ea989a947 /cli
parent08aa61a60172326f4ea70834cd28817c0c397421 (diff)
downloadatoolbox-5a8b44ee3328464a673c2be2e21873fb02f1425b.tar.bz2
Add several more collected scripts
Diffstat (limited to 'cli')
-rwxr-xr-xcli/cleanup-maildir.py533
-rwxr-xr-xcli/dict.py104
-rwxr-xr-xcli/flv2mp4.sh23
-rwxr-xr-xcli/geteltorito.pl228
-rwxr-xr-xcli/i3lock.sh16
-rwxr-xr-xcli/pdf-embed-fonts.sh37
-rwxr-xr-xcli/whatmp3.py370
7 files changed, 1311 insertions, 0 deletions
diff --git a/cli/cleanup-maildir.py b/cli/cleanup-maildir.py
new file mode 100755
index 0000000..e109ea4
--- /dev/null
+++ b/cli/cleanup-maildir.py
@@ -0,0 +1,533 @@
+#!/usr/bin/python -tt
+#
+# Copyright 2004-2006 Nathaniel W. Turner <nate@houseofnate.net>
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+
+"""
+USAGE
+ cleanup-maildir [OPTION].. COMMAND FOLDERNAME..
+
+DESCRIPTION
+ Cleans up old messages in FOLDERNAME; the exact action taken
+ depends on COMMAND. (See next section.)
+ Note that FOLDERNAME is a name such as 'Drafts', and the
+ corresponding maildir path is determined using the values of
+ maildir-root, folder-prefix, and folder-seperator.
+
+COMMANDS
+ archive - move old messages to subfolders based on message date
+ trash - move old message to trash folder
+ delete - permanently delete old messages
+
+OPTIONS
+ -h, --help
+ Show this help.
+ -q, --quiet
+ Suppress normal output.
+ -v, --verbose
+ Output extra information for testing.
+ -n, --trial-run
+ Do not actually touch any files; just say what would be done.
+ -a, --age=N
+ Only touch messages older than N days. Default is 14 days.
+ -k, --keep-flagged-threads
+ If any messages in a thread are flagged, do not touch them or
+ any other messages in that thread.
+ Note: the thread-detection mechanism is currently base purely on
+ a message's subject. The In-Reply-To header is not currently used.
+ -r, --keep-read
+ If any messages are flagged as READ, do not touch them.
+ -t, --trash-folder=F
+ Use F as trash folder when COMMAND is 'trash'.
+ Default is 'Trash'.
+ --archive-folder=F
+ Use F as the base for constructing archive folders. For example, if F is
+ 'Archive', messages from 2004 might be put in the folder 'Archive.2004'.
+ -d, --archive-hierarchy-depth=N
+ Specify number of subfolders in archive hierarchy; 1 is just
+ the year, 2 is year/month (default), 3 is year/month/day.
+ --maildir-root=F
+ Specifies folder that contains mail folders.
+ Default is "$HOME/Maildir".
+ --folder-seperator=str
+ Folder hierarchy seperator. Default is '.'
+ --folder-prefix=str
+ Folder prefix. Default is '.'
+
+NOTES
+ The following form is accepted for backwards compatibility, but is deprecated:
+ cleanup-maildir --mode=COMMAND [OPTION].. FOLDERNAME..
+
+EXAMPLES
+ # Archive messages in 'Sent Items' folder over 30 days old
+ cleanup-maildir --age=30 archive 'Sent Items'"
+
+ # Delete messages over 2 weeks old in 'Lists/debian-devel' folder,
+ # except messages that are part of a thread containing a flagged message.
+ cleanup-maildir --keep-flagged-threads trash 'Lists.debian-devel'
+"""
+
+__version__ = "0.3.0"
+# $Id$
+# $URL$
+
+import mailbox
+import os.path
+import os
+import rfc822
+import string
+import socket
+import time
+import logging
+import sys
+import getopt
+
+
+def mkMaildir(path):
+ """Make a Maildir structure rooted at 'path'"""
+ os.mkdir(path, 0700)
+ os.mkdir(os.path.join(path, 'tmp'), 0700)
+ os.mkdir(os.path.join(path, 'new'), 0700)
+ os.mkdir(os.path.join(path, 'cur'), 0700)
+
+
+class MaildirWriter(object):
+
+ """Deliver messages into a Maildir"""
+
+ path = None
+ counter = 0
+
+ def __init__(self, path=None):
+ """Create a MaildirWriter that manages the Maildir at 'path'
+
+ Arguments:
+ path -- if specified, used as the default Maildir for this object
+ """
+ if path != None:
+ if not os.path.isdir(path):
+ raise ValueError, 'Path does not exist: %s' % path
+ self.path = path
+ self.logger = logging.getLogger('MaildirWriter')
+
+ def deliver(self, msg, path=None):
+ """Deliver a message to a Maildir
+
+ Arguments:
+ msg -- a message object
+ path -- the path of the Maildir; if None, uses default from __init__
+ """
+ if path != None:
+ self.path = path
+ if self.path == None or not os.path.isdir(self.path):
+ raise ValueError, 'Path does not exist'
+ tryCount = 1
+ srcFile = msg.getFilePath();
+ (dstName, tmpFile, newFile, dstFile) = (None, None, None, None)
+ while 1:
+ try:
+ dstName = "%d.%d_%d.%s" % (int(time.time()), os.getpid(),
+ self.counter, socket.gethostname())
+ tmpFile = os.path.join(os.path.join(self.path, "tmp"), dstName)
+ newFile = os.path.join(os.path.join(self.path, "new"), dstName)
+ self.logger.debug("deliver: attempt copy %s to %s" %
+ (srcFile, tmpFile))
+ os.link(srcFile, tmpFile) # Copy into tmp
+ self.logger.debug("deliver: attempt link to %s" % newFile)
+ os.link(tmpFile, newFile) # Link into new
+ except OSError, (n, s):
+ self.logger.critical(
+ "deliver failed: %s (src=%s tmp=%s new=%s i=%d)" %
+ (s, srcFile, tmpFile, newFile, tryCount))
+ self.logger.info("sleeping")
+ time.sleep(2)
+ tryCount += 1
+ self.counter += 1
+ if tryCount > 10:
+ raise OSError("too many failed delivery attempts")
+ else:
+ break
+
+ # Successful delivery; increment deliver counter
+ self.counter += 1
+
+ # For the rest of this method we are acting as an MUA, not an MDA.
+
+ # Move message to cur and restore any flags
+ dstFile = os.path.join(os.path.join(self.path, "cur"), dstName)
+ if msg.getFlags() != None:
+ dstFile += ':' + msg.getFlags()
+ self.logger.debug("deliver: attempt link to %s" % dstFile)
+ os.link(newFile, dstFile)
+ os.unlink(newFile)
+
+ # Cleanup tmp file
+ os.unlink(tmpFile)
+
+
+class MessageDateError(TypeError):
+ """Indicate that the message date was invalid"""
+ pass
+
+
+class MaildirMessage(rfc822.Message):
+
+ """An email message
+
+ Has extra Maildir-specific attributes
+ """
+
+ def getFilePath(self):
+ if sys.hexversion >= 0x020500F0:
+ return self.fp._file.name
+ else:
+ return self.fp.name
+
+ def isFlagged(self):
+ """return true if the message is flagged as important"""
+ import re
+ fname = self.getFilePath()
+ if re.search(r':.*F', fname) != None:
+ return True
+ return False
+
+ def getFlags(self):
+ """return the flag part of the message's filename"""
+ parts = self.getFilePath().split(':')
+ if len(parts) == 2:
+ return parts[1]
+ return None
+
+ def isNew(self):
+ """return true if the message is marked as unread"""
+ # XXX should really be called isUnread
+ import re
+ fname = self.getFilePath()
+ if re.search(r':.*S', fname) != None:
+ return False
+ return True
+
+ def getSubject(self):
+ """get the message's subject as a unicode string"""
+
+ import email.Header
+ s = self.getheader("Subject")
+ try:
+ return u"".join(map(lambda x: x[0].decode(x[1] or 'ASCII', 'replace'),
+ email.Header.decode_header(s)))
+ except(LookupError):
+ return s
+
+ def getSubjectHash(self):
+ """get the message's subject in a "normalized" form
+
+ This currently means lowercasing and removing any reply or forward
+ indicators.
+ """
+ import re
+ import string
+ s = self.getSubject()
+ if s == None:
+ return '(no subject)'
+ return re.sub(r'^(re|fwd?):\s*', '', string.strip(s.lower()))
+
+ def getDateSent(self):
+ """Get the time of sending from the Date header
+
+ Returns a time object using time.mktime. Not very reliable, because
+ the Date header can be missing or spoofed (and often is, by spammers).
+ Throws a MessageDateError if the Date header is missing or invalid.
+ """
+ dh = self.getheader('Date')
+ if dh == None:
+ return None
+ try:
+ return time.mktime(rfc822.parsedate(dh))
+ except ValueError:
+ raise MessageDateError("message has missing or bad Date")
+ except TypeError: # gets thrown by mktime if parsedate returns None
+ raise MessageDateError("message has missing or bad Date")
+ except OverflowError:
+ raise MessageDateError("message has missing or bad Date")
+
+ def getDateRecd(self):
+ """Get the time the message was received"""
+ # XXX check that stat returns time in UTC, fix if not
+ return os.stat(self.getFilePath())[8]
+
+ def getDateSentOrRecd(self):
+ """Get the time the message was sent, fall back on time received"""
+ try:
+ d = self.getDateSent()
+ if d != None:
+ return d
+ except MessageDateError:
+ pass
+ return self.getDateRecd()
+
+ def getAge(self):
+ """Get the number of seconds since the message was received"""
+ msgTime = self.getDateRecd()
+ msgAge = time.mktime(time.gmtime()) - msgTime
+ return msgAge / (60*60*24)
+
+
+class MaildirCleaner(object):
+
+ """Clean a maildir by deleting or moving old messages"""
+
+ __trashWriter = None
+ __mdWriter = None
+ stats = {'total': 0, 'delete': 0, 'trash': 0, 'archive': 0}
+ keepSubjects = {}
+ archiveFolder = None
+ archiveHierDepth = 2
+ folderBase = None
+ folderPrefix = "."
+ folderSeperator = "."
+ keepFlaggedThreads = False
+ trashFolder = "Trash"
+ isTrialRun = False
+ keepRead = False
+
+ def __init__(self, folderBase=None):
+ """Initialize the MaildirCleaner
+
+ Arguments:
+ folderBase -- the directory in which the folders are found
+ """
+ self.folderBase = folderBase
+ self.__mdWriter = MaildirWriter()
+ self.logger = logging.getLogger('MaildirCleaner')
+ self.logger.setLevel(logging.DEBUG)
+
+ def __getTrashWriter(self):
+ if not self.__trashWriter:
+ path = os.path.join(self.folderBase, self.folderPrefix + self.trashFolder)
+ self.__trashWriter = MaildirWriter(path)
+ return self.__trashWriter
+
+ trashWriter = property(__getTrashWriter)
+
+ def scanSubjects(self, folderName):
+ """Scans for flagged subjects"""
+ self.logger.info("Scanning for flagged subjects...")
+ if (folderName == 'INBOX'):
+ path = self.folderBase
+ else:
+ path = os.path.join(self.folderBase, self.folderPrefix + folderName)
+ maildir = mailbox.Maildir(path, MaildirMessage)
+ self.keepSubjects = {}
+ for i, msg in enumerate(maildir):
+ if msg.isFlagged():
+ self.keepSubjects[msg.getSubjectHash()] = 1
+ self.logger.debug("Flagged (%d): %s", i, msg.getSubjectHash())
+ self.logger.info("Done scanning.")
+
+
+ def clean(self, mode, folderName, minAge):
+
+ """Trashes or archives messages older than minAge days
+
+ Arguments:
+ mode -- the cleaning mode. Valid modes are:
+ trash -- moves the messages to a trash folder
+ archive -- moves the messages to folders based on their date
+ delete -- deletes the messages
+ folderName -- the name of the folder on which to operate
+ This is a name like "Stuff", not a filename
+ minAge -- messages younger than minAge days are left alone
+ """
+
+ if not mode in ('trash', 'archive', 'delete'):
+ raise ValueError
+
+ if (self.keepFlaggedThreads):
+ self.scanSubjects(folderName)
+
+ archiveFolder = self.archiveFolder
+ if (archiveFolder == None):
+ if (folderName == 'INBOX'):
+ archiveFolder = ""
+ else:
+ archiveFolder = folderName
+
+ if (folderName == 'INBOX'):
+ path = self.folderBase
+ else:
+ path = os.path.join(self.folderBase, self.folderPrefix + folderName)
+
+ maildir = mailbox.Maildir(path, MaildirMessage)
+
+ fakeMsg = ""
+ if self.isTrialRun:
+ fakeMsg = "(Not really) "
+
+ # Move old messages
+ for i, msg in enumerate(maildir):
+ if self.keepFlaggedThreads == True \
+ and msg.getSubjectHash() in self.keepSubjects:
+ self.log(logging.DEBUG, "Keeping #%d (topic flagged)" % i, msg)
+ else:
+ if (msg.getAge() >= minAge) and ((not self.keepRead) or (self.keepRead and msg.isNew())):
+ if mode == 'trash':
+ self.log(logging.INFO, "%sTrashing #%d (old)" %
+ (fakeMsg, i), msg)
+ if not self.isTrialRun:
+ self.trashWriter.deliver(msg)
+ os.unlink(msg.getFilePath())
+ elif mode == 'delete':
+ self.log(logging.INFO, "%sDeleting #%d (old)" %
+ (fakeMsg, i), msg)
+ if not self.isTrialRun:
+ os.unlink(msg.getFilePath())
+ else: # mode == 'archive'
+ # Determine subfolder path
+ mdate = time.gmtime(msg.getDateSentOrRecd())
+ datePart = str(mdate[0])
+ if self.archiveHierDepth > 1:
+ datePart += self.folderSeperator \
+ + time.strftime("%m-%b", mdate)
+ if self.archiveHierDepth > 2:
+ datePart += self.folderSeperator \
+ + time.strftime("%d-%a", mdate)
+ subFolder = archiveFolder + self.folderSeperator \
+ + datePart
+ sfPath = os.path.join(self.folderBase,
+ self.folderPrefix + subFolder)
+ self.log(logging.INFO, "%sArchiving #%d to %s" %
+ (fakeMsg, i, subFolder), msg)
+ if not self.isTrialRun:
+ # Create the subfolder if needed
+ if not os.path.exists(sfPath):
+ mkMaildir(sfPath)
+ # Deliver
+ self.__mdWriter.deliver(msg, sfPath)
+ os.unlink(msg.getFilePath())
+ self.stats[mode] += 1
+ else:
+ self.log(logging.DEBUG, "Keeping #%d (fresh)" % i, msg)
+ self.stats['total'] += 1
+
+ def log(self, lvl, text, msgObj):
+ """Log some text with the subject of a message"""
+ subj = msgObj.getSubject()
+ if subj == None:
+ subj = "(no subject)"
+ self.logger.log(lvl, text + ": " + subj)
+
+
+# Defaults
+minAge = 14
+mode = None
+
+logging.basicConfig()
+logging.getLogger().setLevel(logging.DEBUG)
+logging.disable(logging.INFO - 1)
+logger = logging.getLogger('cleanup-maildir')
+cleaner = MaildirCleaner()
+
+# Read command-line arguments
+try:
+ opts, args = getopt.getopt(sys.argv[1:],
+ "hqvnrm:t:a:kd:",
+ ["help", "quiet", "verbose", "version", "mode=", "trash-folder=",
+ "age=", "keep-flagged-threads", "keep-read", "folder-seperator=",
+ "folder-prefix=", "maildir-root=", "archive-folder=",
+ "archive-hierarchy-depth=", "trial-run"])
+except getopt.GetoptError, (msg, opt):
+ logger.error("%s\n\n%s" % (msg, __doc__))
+ sys.exit(2)
+output = None
+for o, a in opts:
+ if o in ("-h", "--help"):
+ print __doc__
+ sys.exit()
+ if o in ("-q", "--quiet"):
+ logging.disable(logging.WARNING - 1)
+ if o in ("-v", "--verbose"):
+ logging.disable(logging.DEBUG - 1)
+ if o == "--version":
+ print __version__
+ sys.exit()
+ if o in ("-n", "--trial-run"):
+ cleaner.isTrialRun = True
+ if o in ("-m", "--mode"):
+ logger.warning("the --mode flag is deprecated (see --help)")
+ if a in ('trash', 'archive', 'delete'):
+ mode = a
+ else:
+ logger.error("%s is not a valid command" % a)
+ sys.exit(2)
+ if o in ("-t", "--trash-folder"):
+ cleaner.trashFolder = a
+ if o == "--archive-folder":
+ cleaner.archiveFolder = a
+ if o in ("-a", "--age"):
+ minAge = int(a)
+ if o in ("-k", "--keep-flagged-threads"):
+ cleaner.keepFlaggedThreads = True
+ if o in ("-r", "--keep-read"):
+ cleaner.keepRead = True
+ if o == "--folder-seperator":
+ cleaner.folderSeperator = a
+ if o == "--folder-prefix":
+ cleaner.folderPrefix = a
+ if o == "--maildir-root":
+ cleaner.folderBase = a
+ if o in ("-d", "--archive-hierarchy-depth"):
+ archiveHierDepth = int(a)
+ if archiveHierDepth < 1 or archiveHierDepth > 3:
+ sys.stderr.write("Error: archive hierarchy depth must be 1, " +
+ "2, or 3.\n")
+ sys.exit(2)
+ cleaner.archiveHierDepth = archiveHierDepth
+
+if not cleaner.folderBase:
+ cleaner.folderBase = os.path.join(os.environ["HOME"], "Maildir")
+if mode == None:
+ if len(args) < 1:
+ logger.error("No command specified")
+ sys.stderr.write(__doc__)
+ sys.exit(2)
+ mode = args.pop(0)
+ if not mode in ('trash', 'archive', 'delete'):
+ logger.error("%s is not a valid command" % mode)
+ sys.exit(2)
+
+if len(args) == 0:
+ logger.error("No folder(s) specified")
+ sys.stderr.write(__doc__)
+ sys.exit(2)
+
+logger.debug("Mode is " + mode)
+
+# Clean each folder
+for dir in args:
+ logger.debug("Cleaning up %s..." % dir)
+ cleaner.clean(mode, dir, minAge)
+
+logger.info('Total messages: %5d' % cleaner.stats['total'])
+logger.info('Affected messages: %5d' % cleaner.stats[mode])
+logger.info('Untouched messages: %5d' %
+ (cleaner.stats['total'] - cleaner.stats[mode]))
diff --git a/cli/dict.py b/cli/dict.py
new file mode 100755
index 0000000..6043a3c
--- /dev/null
+++ b/cli/dict.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+#
+# Lookup word from YoDao online dictionary service.
+#
+
+import sys, urllib2, optparse, os
+try:
+ import xml.etree.ElementTree as ElementTree # in python >=2.5
+except ImportError:
+ try:
+ import cElementTree as ElementTree # effbot's C module
+ except ImportError:
+ try:
+ # effbot's pure Python module
+ import elementtree.ElementTree as ElementTree
+ except ImportError:
+ try:
+ # ElementTree API using libxml2
+ import lxml.etree as ElementTree
+ except ImportError:
+ import warnings
+ warnings.warn("could not import ElementTree "
+ "(http://effbot.org/zone/element-index.htm)")
+
+def parseDict(xml):
+ tree = ElementTree.fromstring(xml)
+ word = tree.find('original-query').text
+ customtrans = tree.findall('custom-translation')
+ translist = []
+ for node in customtrans:
+ temp =[]
+ for item in deepFindAll(node,'translation/content'):
+ temp.append(item.text)
+ translist.append([node.find('source/name').text,temp])
+ return word, translist
+
+def parseSentence(xml):
+ tree = ElementTree.fromstring(xml)
+ senlist = []
+ for node in deepFindAll(tree,'example-sentences/sentence-pair'):
+ senlist.append([node.find('sentence').text,
+ node.find('sentence-translation').text])
+ return senlist
+
+def deepFindAll(element, tag):
+ if type(tag) == type(''): tag = tag.split('/')
+ if tag == []: return [element]
+ if len(tag) == 1:
+ elist = []
+ findres = element.findall(tag[0])
+ if findres: elist.extend(findres)
+ for node in element:
+ elist.extend(deepFindAll(node, tag[0]))
+ return elist
+ else:
+ sublist = deepFindAll(element, tag[0])
+ return deepFindAll(element, tag[1:])
+
+if __name__=='__main__':
+ parser = optparse.OptionParser()
+ parser.add_option('-w', dest='word',action='store_true',
+ default=False, help='print the translation of the word.')
+ parser.add_option('-s', dest='sent',action='store_true',
+ default=False, help='print sample sentences.')
+ options, args = parser.parse_args(sys.argv[1:])
+ #test if the string contains chinese
+ #if ' '.join(args).isalpha():
+ # #os.system('echo %s |festival --tts' %' '.join(args))
+ # os.system('espeak -ven+13 %s &>/dev/null' %' '.join(args))
+ #get word translation
+ xml1= urllib2.urlopen("http://dict.yodao.com/search?keyfrom=dict.python&q=" + '+'.join(args) + "&xmlDetail=true&doctype=xml").read()
+ word, translist = parseDict(xml1)
+ #get sample sentences
+ xml2= urllib2.urlopen("http://dict.yodao.com/search?keyfrom=dict.python&q=lj:" + '+'.join(args) + "&xmlDetail=true&doctype=xml").read()
+ senlist = parseSentence(xml2)
+ #define colors
+ BOLD='\033[1m'
+ DEFAULT='\033[m'
+ UNDERLINE='\033[4m'
+ MAGENTA='\033[35m'
+ YELLOW='\033[33m'
+ GREEN='\033[32m'
+ RED='\033[31m'
+ WHITE='\033[37m'
+ BGWHITE='\033[47m'
+ BLUE='\033[34m'
+ if options.word:
+ print RED+BOLD+word+DEFAULT
+ for item in translist:
+ print MAGENTA+BGWHITE+item[0]+DEFAULT +': '\
+ +GREEN+BOLD+ '; '.join(item[1]) + DEFAULT
+ if options.sent:
+ for item in senlist:
+ print item[0].replace('<b>', YELLOW+BOLD).replace('</b>', DEFAULT)
+ print BLUE+BOLD+item[1]+DEFAULT
+ if not options.word and not options.sent:
+ print RED+BOLD+word+DEFAULT
+ for item in translist:
+ print MAGENTA+BGWHITE+item[0]+DEFAULT +': '\
+ +GREEN+BOLD+ '; '.join(item[1]) + DEFAULT
+ for item in senlist[:7]:
+ print item[0].replace('<b>', YELLOW+BOLD).replace('</b>', DEFAULT)
+ print BLUE+BOLD+item[1]+DEFAULT
diff --git a/cli/flv2mp4.sh b/cli/flv2mp4.sh
new file mode 100755
index 0000000..f331c88
--- /dev/null
+++ b/cli/flv2mp4.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+#
+# Convert FLV to MP4 with ffmpeg and preserving original encodings.
+#
+# Credit:
+# [1] FFMPEG convert flv to mp4 without losing quality
+# http://superuser.com/a/727081
+#
+# Aaron LI
+# Created: 2015-12-28
+#
+
+if [ $# -ne 1 ] && [ $# -ne 2 ]; then
+ echo "Usage: `basename $0` <file.flv> [file.mp4]"
+ exit 1
+fi
+
+INFILE="$1"
+OUTFILE="$2"
+[ -z "${OUTFILE}" ] && OUTFILE="${INFILE%.flv}.mp4"
+
+ffmpeg -i "${INFILE}" -codec copy "${OUTFILE}"
+
diff --git a/cli/geteltorito.pl b/cli/geteltorito.pl
new file mode 100755
index 0000000..fec18fa
--- /dev/null
+++ b/cli/geteltorito.pl
@@ -0,0 +1,228 @@
+#!/usr/bin/perl
+
+use Getopt::Std;
+
+#
+# geteltorito.pl: a bootimage extractor
+# Script that will extract the first El Torito bootimage from a
+# bootable CD image
+# R. Krienke 08/2001
+# krienke@uni-koblenz.de
+# License: GPL
+#
+# Get latest version from:
+# http://userpages.uni-koblenz.de/~krienke/ftp/noarch/geteltorito
+#
+$utilVersion="0.6";
+#
+# Version 0.6
+# 2015/02/25
+# I included a patch by Daniel Kekez, daniel.kekez@utoronto.ca to make geteltorito
+# better compatible with windows:
+# "To run this Perl script using using Strawberry Perl under Windows, I
+# found it was necessary to explicitly open the files in binary mode since
+# Windows will default to text mode when open() is called."
+# Version 0.5
+# 2009/06/22
+# A patch for harddisk emulation images from <colimit@gmail.com>.
+# For BootMediaType=4 (harddisk emulation) SectorCount is always 1, and geteltorito.pl
+# returns just MBR. This patch guesses the correct bootimage size
+# from MBR (offset+size of the first partitition).
+# Version 0.4
+# 2007/02/01
+# A patch from Santiago Garcia <manty@debian.org> to use a virtual sector
+# size (vSecSize) of 512 bytes, as defined on "El Torito" specs and change
+# unpack of the sector count from n to v to get the correct sector count.
+# Version 0.3
+# 2006/02/21
+# A patch from Ben Collins <bcollins@ubuntu.com> to make the
+# utility work on PPC machines (change from 'L'-encoding in pack to 'V')
+# Version 0.2
+# Several patches included from Nathan Stratton Treadway(nathant@ontko.com)
+# to adjust the platform output as well as fixes for other minor bugs
+# Version 0.1
+# Initial release
+#
+# For information on El Torito see
+# http://en.wikipedia.org/wiki/El_torito
+
+$vSecSize=512;
+$secSize=2048;
+$ret=undef;$version=undef;$opt_h=undef;$loadSegment=undef;$systemType=undef;
+
+#
+# Read a particular sector from a file
+# sector counting starts at 0, not 1
+#
+sub getSector{
+ my ($secNum, $secCount, $file)=@_;
+ my ($sec, $count);
+
+ open(FILE, "<:raw", $file) || die "Cannot open \"$file\" \n";
+
+ seek(FILE, $secNum*$secSize, 0);
+ $count=read(FILE, $sec, $vSecSize*$secCount, 0) ;
+ if( $count != $vSecSize*$secCount ){
+ warn "Error reading from file \"$file\"\n";
+ }
+ close(FILE);
+
+ return($sec);
+}
+
+
+#
+# Write eltorito data into a file
+#
+sub writeOutputFile{
+ my($name)=shift;
+ my($value)=shift;
+
+ open(OUT, ">:raw", $name)|| die "$0: Cannot open outputfile \"$name\" for writing. Stop.";
+ print OUT $value;
+ close(OUT);
+}
+
+
+#
+# Usage
+#
+sub usage{
+ warn "\n$0 [-hv] [-o outputfilename] cd-image \n",
+ "Script will try to extract an El Torito image from a \n",
+ "bootable CD (or cd-image) given by <cd-image> and write \n",
+ "the data extracted to STDOUT or to a file.\n",
+ " -h: This help. \n",
+ " -v: Print version of script and exit.\n",
+ " -o <file>: Write extracted data to file <file> instead of STDOUT.\n",
+ "\n\n";
+ exit 0;
+}
+
+
+# ---------------------------------------------------------------------
+$ret=getopts('hvo:');
+
+if( defined($opt_v) ){
+ warn "Version: $utilVersion \n";
+ exit 0;
+}
+
+if( defined($opt_h) || $#ARGV <0 ){
+ usage(0);
+}
+
+if( defined($opt_o) ){
+ $outputFilename="$opt_o";
+}
+
+$imageFile=$ARGV[0];
+
+if( ! -r $imageFile ){
+ die "Cannot read image/device \"$imageFile\". Aborting\n";
+}
+
+#
+# Read Sector 17 from CD which should contain a Boot Record Volume
+# descriptor. This descriptor contains at its start the text ($isoIdent)
+# CD001 and ($toritoSpec)
+# EL TORITO SPECIFICATION
+# see http://www.cdpage.com/Compact_Disc_Variations/eltoritoi.html
+# for details
+#
+
+$sector=getSector(17, 1, $imageFile );
+($boot, $isoIdent, $version, $toritoSpec,
+ $unUsed, $bootP)= unpack( "Ca5CA32A32V", $sector );
+
+if( $isoIdent ne "CD001" || $toritoSpec ne "EL TORITO SPECIFICATION" ){
+ die "This data image does not seem to be a bootable CD-image\n";
+}
+
+#
+# Now fetch the sector of the booting catalog
+#
+$sector=getSector($bootP, 1, $imageFile );
+
+print STDERR "Booting catalog starts at sector: $bootP \n";
+
+# The first 32 bytes of this sector contains the validation entry for a
+# boot. The first byte has to be 01, the next byte determines the
+# architecture the image is designed for, where 00 is i86, 01 is PowerPC
+# and 02 is Mac. More data give info about manufacturer, etc. The
+# final two bytes must contain 0x55 and 0xAA respectively (as
+# defined by the El Torito standard).
+
+$validateEntry=substr($sector, 0, 32);
+
+($header, $platform, $unUsed, $manufact, $unUsed, $five, $aa)=
+ unpack( "CCvA24vCC", $validateEntry);
+
+if( $header != 1 || $five != 0x55 || $aa != 0xaa ){
+ die "Invalid Validation Entry on image \n";
+}
+
+print STDERR "Manufacturer of CD: $manufact\n";
+print STDERR "Image architecture: ";
+print STDERR "x86" if( $platform == 0 );
+print STDERR "PowerPC" if( $platform == 1 );
+print STDERR "Mac" if( $platform == 2 );
+print STDERR "unknown ($platform)" if( $platform > 2 );
+print STDERR "\n";
+
+#
+# Now we examine the initial/defaultentry which follows the validate
+# entry and has a size of 32 bytes.
+
+$initialEntry=substr($sector, 32, 32);
+
+($boot, $media, $loadSegment, $systemType, $unUsed,
+ $sCount, $imgStart, $unUsed)=unpack( "CCvCCvVC", $initialEntry);
+
+if( $boot != 0x88 ){
+ die "Boot indicator in Initial/Default-Entry is not 0x88. CD is not bootable. \n";
+}
+
+print STDERR "Boot media type is: ";
+if( $media == 0 ){
+ print STDERR "no emulation";
+ $count=0;
+}
+if( $media == 1 ){
+ print STDERR "1.2meg floppy";
+ $count=1200*1024/$vSecSize;
+}
+if( $media == 2 ){
+ print STDERR "1.44meg floppy";
+ $count=1440*1024/$vSecSize;
+}
+if( $media == 3 ){
+ print STDERR "2.88meg floppy";
+ $count=2880*1024/$vSecSize;
+}
+if( $media == 4 ){
+ print STDERR "harddisk";
+ $MBR=getSector($imgStart, 1, $imageFile );
+ $partition1=substr($MBR, 446, 16);
+ ($unUsed, $firstSector, $partitionSize) = unpack( "A8VV", $partition1);
+ $count=$firstSector + $partitionSize;
+}
+print STDERR "\n";
+
+# Only use the internal sector counter if the real size is unknown
+# ($count==0)
+$cnt=$count==0?$sCount:$count;
+
+print STDERR "El Torito image starts at sector $imgStart and has $cnt sector(s) of $vSecSize Bytes\n";
+
+# We are there:
+# Now read the bootimage to stdout
+$image=getSector($imgStart, $cnt, $imageFile);
+
+if( length($outputFilename) ){
+ writeOutputFile($outputFilename, $image);
+ print STDERR "\nImage has been written to file \"$outputFilename\".\n";
+}else{
+ print "$image";
+ print STDERR "Image has been written to stdout ....\n";
+}
diff --git a/cli/i3lock.sh b/cli/i3lock.sh
new file mode 100755
index 0000000..1af93d6
--- /dev/null
+++ b/cli/i3lock.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+#
+# see i3lock(1)
+#
+# Aaron LI
+# 2017-10-03
+#
+
+revert() {
+ xset dpms 0 0 0
+}
+
+trap revert SIGHUP SIGINT SIGTERM
+xset +dpms dpms 5 5 5
+i3lock -n
+revert
diff --git a/cli/pdf-embed-fonts.sh b/cli/pdf-embed-fonts.sh
new file mode 100755
index 0000000..0f89f70
--- /dev/null
+++ b/cli/pdf-embed-fonts.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+#
+# Embed a PDF file with all fonts it uses, so it can be rendered and printed
+# correctly by every PDF reader on all types of OS.
+#
+#
+# Credits:
+# [1] The PDF viewer 'Evince' on Linux can not display some math symbols correctly
+# http://stackoverflow.com/a/10282269/4856091
+#
+#
+# Aaron LI
+# 2015-10-24
+#
+
+usage() {
+ echo "usage: `basename $0` <input.pdf> [ output_embedfonts.pdf ]"
+ exit 1
+}
+
+if [ $# -eq 0 ]; then
+ usage
+fi
+case "$1" in
+ -[hH]*)
+ usage
+ ;;
+esac
+
+INFILE="$1"
+OUTFILE="$2"
+
+[ -z "${OUTFILE}" ] && OUTFILE="${INFILE%.pdf}_embedfonts.pdf"
+
+# "-dPDFSETTINGS=/prepress" tells Ghostscript to embed all non-embedded fonts.
+gs -o "${OUTFILE}" -dPDFSETTINGS=/prepress -sDEVICE=pdfwrite "${INFILE}"
+
diff --git a/cli/whatmp3.py b/cli/whatmp3.py
new file mode 100755
index 0000000..4269fd0
--- /dev/null
+++ b/cli/whatmp3.py
@@ -0,0 +1,370 @@
+#!/usr/bin/env python3
+
+import argparse
+import multiprocessing
+import os
+import re
+import shutil
+import sys
+import threading
+from fnmatch import fnmatch
+
+VERSION = "3.8"
+
+# DEFAULT CONFIGURATION
+
+# Output folder unless specified
+# output = os.path.join(os.environ['HOME'], "Desktop/")
+output = os.getcwd()
+
+# Separate torrent output folder (defaults to output):
+torrent_dir = output
+
+# Do you want to copy additional files (.jpg, .log, etc)?
+copyother = 1
+
+# Do you want to zeropad tracknumbers? (1 => 01, 2 => 02 ...)
+zeropad = 1
+
+# Do you want to dither FLACs to 16/44 before encoding?
+dither = 0
+
+# Specify tracker announce URL
+tracker = None
+
+# Max number of threads (e.g., Dual-core = 2, Hyperthreaded Dual-core = 4)
+max_threads = multiprocessing.cpu_count()
+
+# Tags to copy (note: changing/adding to these requires changing/adding values in/to 'encoders' below)
+copy_tags = ('TITLE', 'ALBUM', 'ARTIST', 'TRACKNUMBER', 'GENRE', 'COMMENT', 'DATE')
+
+# Default encoding options
+enc_opts = {
+ '320': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -b 320 --ignore-tag-errors --noreplaygain'},
+ 'V0': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -V 0 --vbr-new --ignore-tag-errors --noreplaygain'},
+ 'V2': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -V 2 --vbr-new --ignore-tag-errors --noreplaygain'},
+ 'V8': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -V 8 --vbr-new --ignore-tag-errors --noreplaygain'},
+ 'Q8': {'enc': 'oggenc', 'ext': '.ogg', 'opts': '-q 8 --utf8'},
+ 'AAC': {'enc': 'neroAacEnc', 'ext': '.aac', 'opts': '-br 320000'},
+ 'ALAC': {'enc': 'ffmpeg', 'ext': '.m4a', 'opts': ''},
+ 'FLAC': {'enc': 'flac', 'ext': '.flac', 'opts': '--best'}
+}
+
+encoders = {
+ 'lame': {
+ 'enc': "lame --silent %(opts)s %(tags)s --add-id3v2 - '%(filename)s' 2>&1",
+ 'TITLE': "--tt '%(TITLE)s'",
+ 'ALBUM': "--tl '%(ALBUM)s'",
+ 'ARTIST': "--ta '%(ARTIST)s'",
+ 'TRACKNUMBER': "--tn '%(TRACKNUMBER)s'",
+ 'GENRE': "--tg '%(GENRE)s'",
+ 'DATE': "--ty '%(DATE)s'",
+ 'COMMENT': "--tc '%(COMMENT)s'",
+ 'regain': "mp3gain -q -c -s i '%s'/*.mp3"
+ },
+ 'oggenc': {
+ 'enc': "oggenc -Q %(opts)s %(tags)s -o '%(filename)s' - 2>&1",
+ 'TITLE': "-t '%(TITLE)s'",
+ 'ALBUM': "-l '%(ALBUM)s'",
+ 'ARTIST': "-a '%(ARTIST)s'",
+ 'TRACKNUMBER': "-N '%(TRACKNUMBER)s'",
+ 'GENRE': "-G '%(GENRE)s'",
+ 'DATE': "-d '%(DATE)s'",
+ 'COMMENT': "-c 'comment=%(COMMENT)s'",
+ 'regain': "vorbisgain -qafrs '%s'/*.ogg"
+ },
+ 'neroAacEnc': {
+ 'enc': "neroAacEnc %(opts)s -if - -of '%(filename)s' 2>&1 && neroAacTag %(tags)s",
+ 'TITLE': "-meta:title='%(TITLE)s'",
+ 'ALBUM': "-meta:album='%(ALBUM)s'",
+ 'ARTIST': "-meta:artist='%(ARTIST)s'",
+ 'TRACKNUMBER': "-meta:track='%(TRACKNUMBER)s'",
+ 'GENRE': "-meta:genre='%(GENRE)s'",
+ 'DATE': "-meta:year='%(DATE)s'",
+ 'COMMENT': "-meta:comment='%(COMMENT)s'",
+ 'regain': "aacgain -q -c '%s'/*.aac"
+ },
+ 'ffmpeg': {
+ 'enc': "ffmpeg %(opts)s -i - -acodec alac %(tags)s '%(filename)s' 2>&1",
+ 'TITLE': "-metadata title='%(TITLE)s'",
+ 'ALBUM': "-metadata album='%(ALBUM)s'",
+ 'ARTIST': "-metadata author='%(ARTIST)s'",
+ 'TRACKNUMBER': "-metadata track='%(TRACKNUMBER)s'",
+ 'GENRE': "-metadata genre='%(GENRE)s'",
+ 'DATE': "-metadata date='%(DATE)s'",
+ 'COMMENT': "-metadata comment='%(COMMENT)s'",
+ 'regain': ""
+ },
+ 'flac': {
+ 'enc': "flac %(opts)s -s %(tags)s -o '%(filename)s' - 2>&1",
+ 'TITLE': "-T 'TITLE=%(TITLE)s'",
+ 'ALBUM': "-T 'ALBUM=%(ALBUM)s'",
+ 'ARTIST': "-T 'ARTIST=%(ARTIST)s'",
+ 'TRACKNUMBER': "-T 'TRACKNUMBER=%(TRACKNUMBER)s'",
+ 'GENRE': "-T 'GENRE=%(GENRE)s'",
+ 'DATE': "-T 'DATE=%(DATE)s'",
+ 'COMMENT': "-T 'COMMENT=%(COMMENT)s'",
+ 'regain': "metaflac --add-replay-gain '%s'/*.flac"
+ }
+}
+
+dither_cmd = 'sox -t wav - -b 16 -t wav - rate 44100 dither'
+
+# END CONFIGURATION
+
+codecs = []
+
+def copy_other(opts, flacdir, outdir):
+ if opts.verbose:
+ print('COPYING other files')
+ for dirpath, dirs, files in os.walk(flacdir, topdown=False):
+ for name in files:
+ if opts.nolog and fnmatch(name.lower(), '*.log'):
+ continue
+ if opts.nocue and fnmatch(name.lower(), '*.cue'):
+ continue
+ if opts.nodots and fnmatch(name.lower(), '^.'):
+ continue
+ if (not fnmatch(name.lower(), '*.flac')
+ and not fnmatch(name.lower(), '*.m3u')):
+ d = re.sub(re.escape(flacdir), outdir, dirpath)
+ if (os.path.exists(os.path.join(d, name))
+ and not opts.overwrite):
+ continue
+ if not os.path.exists(d):
+ os.makedirs(d)
+ shutil.copy(os.path.join(dirpath, name), d)
+
+class EncoderArg(argparse.Action):
+ def __init__(self, option_strings, dest, nargs=None, **kwargs):
+ super(EncoderArg, self).__init__(option_strings, dest, nargs, **kwargs)
+ def __call__(self, parser, namespace, values, option_string=None):
+ codecs.append(option_string[2:])
+
+def escape_quote(pattern):
+ pattern = re.sub("'", "'\"'\"'", pattern)
+ return pattern
+
+def escape_percent(pattern):
+ pattern = re.sub('%', '%%', pattern)
+ return pattern
+
+def failure(r, msg):
+ print("ERROR: %s: %s" % (r, msg), file=sys.stderr)
+
+def make_torrent(opts, target):
+ if opts.verbose:
+ print('MAKE: %s.torrent' % os.path.relpath(target))
+ torrent_cmd = "mktorrent -p -a '%s' -o '%s.torrent' '%s' 2>&1" % (
+ opts.tracker, escape_quote(os.path.join(opts.torrent_dir,
+ os.path.basename(target))),
+ escape_quote(target)
+ )
+ if opts.additional:
+ torrent_cmd += ' ' + opts.additional
+ if opts.nodate:
+ torrent_cmd += ' -d'
+ if not opts.verbose:
+ torrent_cmd += ' >/dev/null'
+ if opts.verbose:
+ print(torrent_cmd)
+ r = system(torrent_cmd)
+ if r: failure(r, torrent_cmd)
+
+def replaygain(opts, codec, outdir):
+ if opts.verbose:
+ print("APPLYING replaygain")
+ print(encoders[enc_opts[codec]['enc']]['regain'] % outdir)
+ r = system(encoders[enc_opts[codec]['enc']]['regain'] % escape_quote(outdir))
+ if r: failure(r, "replaygain")
+ for dirpath, dirs, files in os.walk(outdir, topdown=False):
+ for name in dirs:
+ r = system(encoders[enc_opts[codec]['enc']]['regain']
+ % os.path.join(dirpath, name))
+ if r: failure(r, "replaygain")
+
+def setup_parser():
+ p = argparse.ArgumentParser(
+ description="whatmp3 transcodes audio files and creates torrents for them",
+ argument_default=False,
+ epilog="""depends on flac, metaflac, mktorrent, and optionally oggenc, lame, neroAacEnc,
+ neroAacTag, mp3gain, aacgain, vorbisgain, and sox""")
+ p.add_argument('--version', action='version', version='%(prog)s ' + VERSION)
+ for a in [
+ [['-v', '--verbose'], False, 'increase verbosity'],
+ [['-n', '--notorrent'], False, 'do not create a torrent after conversion'],
+ [['-r', '--replaygain'], False, 'add ReplayGain to new files'],
+ [['-c', '--original'], False, 'create a torrent for the original FLAC'],
+ [['-i', '--ignore'], False, 'ignore top level directories without flacs'],
+ [['-s', '--silent'], False, 'do not write to stdout'],
+ [['-S', '--skipgenre'], False, 'do not insert a genre tag in MP3 files'],
+ [['-D', '--nodate'], False, 'do not write the creation date to the .torrent file'],
+ [['-L', '--nolog'], False, 'do not copy log files after conversion'],
+ [['-C', '--nocue'], False, 'do not copy cue files after conversion'],
+ [['-H', '--nodots'], False, 'do not copy dot/hidden files after conversion'],
+ [['-w', '--overwrite'], False, 'overwrite files in output dir'],
+ [['-d', '--dither'], dither, 'dither FLACs to 16/44 before encoding'],
+ [['-m', '--copyother'], copyother, 'copy additional files (def: true)'],
+ [['-z', '--zeropad'], zeropad, 'zeropad tracknumbers (def: true)'],
+ ]:
+ p.add_argument(*a[0], **{'default': a[1], 'action': 'store_true', 'help': a[2]})
+ for a in [
+ [['-a', '--additional'], None, 'ARGS', 'additional arguments to mktorrent'],
+ [['-t', '--tracker'], tracker, 'URL', 'tracker URL'],
+ [['-o', '--output'], output, 'DIR', 'set output dir'],
+ [['-O', '--torrent-dir'], torrent_dir, 'DIR', 'set independent torrent output dir'],
+ ]:
+ p.add_argument(*a[0], **{
+ 'default': a[1], 'action': 'store',
+ 'metavar': a[2], 'help': a[3]
+ })
+ p.add_argument('-T', '--threads', default=max_threads, action='store',
+ dest='max_threads', type=int, metavar='THREADS',
+ help='set number of threads THREADS (def: %s)' % max_threads)
+ for enc_opt in enc_opts.keys():
+ p.add_argument("--" + enc_opt, action=EncoderArg, nargs=0,
+ help='convert to %s' % (enc_opt))
+ p.add_argument('flacdirs', nargs='+', metavar='flacdir',
+ help='directories to transcode')
+ return p
+
+def system(cmd):
+ return os.system(cmd)
+
+def transcode(f, flacdir, mp3_dir, codec, opts, lock):
+ tags = {}
+ for tag in copy_tags:
+ tagcmd = "metaflac --show-tag='" + escape_quote(tag) + \
+ "' '" + escape_quote(f) + "'"
+ t = re.sub('\S.+?=', '', os.popen(tagcmd).read().rstrip(), count=1)
+ if t:
+ tags.update({tag: escape_quote(t)})
+ del t
+ if (opts.zeropad and 'TRACKNUMBER' in tags
+ and len(tags['TRACKNUMBER']) == 1):
+ tags['TRACKNUMBER'] = '0' + tags['TRACKNUMBER']
+ if opts.skipgenre and 'GENRE' in tags: del tags['GENRE']
+
+ outname = re.sub(re.escape(flacdir), mp3_dir, f)
+ outname = re.sub(re.compile('\.flac$', re.IGNORECASE), '', outname)
+ with lock:
+ if not os.path.exists(os.path.dirname(outname)):
+ os.makedirs(os.path.dirname(outname))
+ outname += enc_opts[codec]['ext']
+ if os.path.exists(outname) and not opts.overwrite:
+ print("WARN: file %s already exists" % (os.path.relpath(outname)),
+ file=sys.stderr)
+ return 1
+ flac_cmd = encoders[enc_opts[codec]['enc']]['enc']
+ tagline = ''
+ for tag in tags:
+ tagline = tagline + " " + encoders[enc_opts[codec]['enc']][tag]
+ tagline = tagline % tags
+ if opts.dither:
+ flac_cmd = dither_cmd + ' | ' + flac_cmd
+ flac_cmd = "flac -sdc -- '" + escape_percent(escape_quote(f)) + \
+ "' | " + flac_cmd
+ flac_cmd = flac_cmd % {
+ 'opts': enc_opts[codec]['opts'],
+ 'filename': escape_quote(outname),
+ 'tags': tagline
+ }
+ outname = os.path.basename(outname)
+ if not opts.silent:
+ print("encoding %s" % outname)
+ if opts.verbose:
+ print(flac_cmd)
+ r = system(flac_cmd)
+ if r:
+ failure(r, "error encoding %s" % outname)
+ system("touch '%s/FAILURE'" % mp3_dir)
+ return 0
+
+class Transcode(threading.Thread):
+ def __init__(self, file, flacdir, mp3_dir, codec, opts, lock, cv):
+ threading.Thread.__init__(self)
+ self.file = file
+ self.flacdir = flacdir
+ self.mp3_dir = mp3_dir
+ self.codec = codec
+ self.opts = opts
+ self.lock = lock
+ self.cv = cv
+
+ def run(self):
+ r = transcode(self.file, self.flacdir, self.mp3_dir, self.codec,
+ self.opts, self.lock)
+ with self.cv:
+ self.cv.notify_all()
+ return r
+
+def main():
+ parser = setup_parser()
+ opts = parser.parse_args()
+ if not opts.output.endswith('/'):
+ opts.output += '/'
+ if len(codecs) == 0 and not opts.original:
+ parser.error("you must provide at least one format to transcode to")
+ exit()
+ for flacdir in opts.flacdirs:
+ flacdir = os.path.abspath(flacdir)
+ flacfiles = []
+ if not os.path.exists(opts.torrent_dir):
+ os.makedirs(opts.torrent_dir)
+ for dirpath, dirs, files in os.walk(flacdir, topdown=False):
+ for name in files:
+ if fnmatch(name.lower(), '*.flac'):
+ flacfiles.append(os.path.join(dirpath, name))
+ flacfiles.sort()
+ if opts.ignore and not flacfiles:
+ if not opts.silent:
+ print("SKIP (no flacs in): %s" % (os.path.relpath(flacdir)))
+ continue
+ if opts.original:
+ if not opts.silent:
+ print('BEGIN ORIGINAL FLAC')
+ if opts.output and opts.tracker and not opts.notorrent:
+ make_torrent(opts, flacdir)
+ if not opts.silent:
+ print('END ORIGINAL FLAC')
+
+ for codec in codecs:
+ outdir = os.path.basename(flacdir)
+ flacre = re.compile('FLAC', re.IGNORECASE)
+ if flacre.search(outdir):
+ outdir = flacre.sub(codec, outdir)
+ else:
+ outdir = outdir + " (" + codec + ")"
+ outdir = opts.output + outdir
+ if not os.path.exists(outdir):
+ os.makedirs(outdir)
+
+ if not opts.silent:
+ print('BEGIN ' + codec + ': %s' % os.path.relpath(flacdir))
+ threads = []
+ cv = threading.Condition()
+ lock = threading.Lock()
+ for f in flacfiles:
+ with cv:
+ while (threading.active_count() == max(1, opts.max_threads) + 1):
+ cv.wait()
+ t = Transcode(f, flacdir, outdir, codec, opts, lock, cv)
+ t.start()
+ threads.append(t)
+ for t in threads:
+ t.join()
+
+ if opts.copyother:
+ copy_other(opts, flacdir, outdir)
+ if opts.replaygain:
+ replaygain(opts, codec, outdir)
+ if opts.output and opts.tracker and not opts.notorrent:
+ make_torrent(opts, outdir)
+ if not opts.silent:
+ print('END ' + codec + ': %s' % os.path.relpath(flacdir))
+
+ if opts.verbose: print('ALL DONE: ' + os.path.relpath(flacdir))
+ return 0
+
+if __name__ == '__main__':
+ main()