diff options
author | Aaron LI <aly@aaronly.me> | 2017-11-03 18:45:20 +0800 |
---|---|---|
committer | Aaron LI <aly@aaronly.me> | 2017-11-03 18:45:20 +0800 |
commit | 5a8b44ee3328464a673c2be2e21873fb02f1425b (patch) | |
tree | 192c3cc96018a26a85ec470aad1f814ea989a947 /cli | |
parent | 08aa61a60172326f4ea70834cd28817c0c397421 (diff) | |
download | atoolbox-5a8b44ee3328464a673c2be2e21873fb02f1425b.tar.bz2 |
Add several more collected scripts
Diffstat (limited to 'cli')
-rwxr-xr-x | cli/cleanup-maildir.py | 533 | ||||
-rwxr-xr-x | cli/dict.py | 104 | ||||
-rwxr-xr-x | cli/flv2mp4.sh | 23 | ||||
-rwxr-xr-x | cli/geteltorito.pl | 228 | ||||
-rwxr-xr-x | cli/i3lock.sh | 16 | ||||
-rwxr-xr-x | cli/pdf-embed-fonts.sh | 37 | ||||
-rwxr-xr-x | cli/whatmp3.py | 370 |
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() |