>
Для более комфортной работы с шаблонами ExpressionEngine я использую Mountee. Mountee позволяет подключиться к ЕЕ так, чтобы шаблоны были представлены как обычные файлы в системе, и одновременно записывать все изменения сразу в БД, без лишнего этапа ручной синхронизации шаблонов.
Я, как и многие маководы, практически не выключаю компьютер, а просто “усыпляю” его закрытием крышки. Поэтому основной проблемой в данном воркфлоу для меня стала необходимость заново коннектиться к ЕЕ с помощью Mountee после пробуждения компьютера, поскольку Mountee терял соединение, и изменения в шаблонах переставали сохраняться.
Поскольку я существо ленивое, мне не хочется каждый раз делать это вручную, да и вспоминаю я об этом не всегда вовремя. Значит, данную задачу необходимо автоматизировать. Однако мои главные инструменты в почётном деле автоматизации всего и вся - AppleScript и bash - оказались бессильны перед нескриптуемым Mountee.
Ну да не эпплскриптом единым :) Для автоматизации этой маленькой рутины будем использовать Sikuli - очень необычную IDE, которую я давненько хотела попробовать, но как-то не было подходящих задач.
Вот что пишут о Sikuli на официальном сайте:
Sikuli is a visual technology to automate and test graphical user interfaces (GUI) using images (screenshots). Sikuli includes Sikuli Script, a visual scripting API for Jython, and Sikuli IDE, an integrated development environment for writing visual scripts with screenshots easily. Sikuli Script automates anything you see on the screen without internal API’s support. You can programmatically control a web page, a Windows/Linux/Mac OS X desktop application, or even an iphone or android application running in a simulator or via VNC.
На самом деле, это очень крутая штука для автоматизации всех тех процессов, которые не могуть быть автоматизированы обычными средствами. Да что там процессы! Sikuli умеет играть в Angry Birds!
Итак, пишем скрипт, который будет:
Для одного из проектов мне нужно было сделать небольшой календарик, который позволял бы, во-первых, перелистывать месяцы без перезагрузки страниц, а во-вторых, при наведении на дату, содержащую какое-либо событие, красиво отображать его (события) название и время проведения. Проект, для которого нужен был этот календарь, сделан на ExpressionEngine, а сам календарик был сделан с помощью модуля Solspace Calendar.
Поскольку я мастер творческого копи-паста, а также стойкий сторонник теории, что всё уже придумано до меня, я отправилась бороздить просторы форума Solspace в поисках готового решения данной задачи. К своему удивлению, обнаружила всего 2 поста с впоросами на данную тему, но ни одного кусочка кода, который можно было бы использовать.
Google радушно предложил ссылку на двухлетней давности запись в блоге некоего Kyle Truscott, который решал сходную задачу с помощью встроенного календаря EE. На основе его кода я сделала свой для календаря, сделанного на Solspace Calendar.
Вот что получилось в итоге:

Посмотреть календарь в действии можно тут
Поскольку у меня всё не как у людей, то и просто делать что-либо по хозяйству я не могу. Всё мне нужно автоматизировать, а если полностью автоматизировать невозможно, то хотя бы по максимум сократить количество рутинных операций.
Я не могу похвастаться каким-либо кулинарным мастерством. Готовить по рецептам умею, а вот кулинарной фантазии у меня нет совсем. И я очень завидую тем хозайкам, которые, открыв холодильник и окинув его недра беглым взглядом, в состоянии тут же придумать салат, причёску и трагедию обед или ужин из трёх блюд с десертами. Единственным для меня способом избежать ситуации, в которой моя семья каждый день ест макароны, сосиски и пельмени (потому что составляющих для интересного рецепта либо нет, либо они не были вовремя разморожены), это заранее планировать меню на неделю вперёд.
На тему meal planning в сети есть столько информации, что кажется, будто это очень легко, и главное - только начать. Казалось, что проблем быть не должно.
Каждый месяц я покупаю 3-4 журнала с рецептами. Обычно это “Гастроном”, “Хлеб Соль”, “Вкусно и полезно” и “Savours”. И в каждом номере есть рецепты, которые я хотела бы попробовать. И есть, разумеется, интернет сайты с рецептами, где тоже есть много всего интересного. Но это обилие рецептов не идёт мне на пользу - я никогда не помню, где именно искать тот рецепт, что я хотела попробовать, - это если я вообще вспомню о существовании такого рецепта. Но даже если предположить, что я вспомнила про рецепт, это ещё не значит, что мне удастся добавить его в план - например, на позапрошлой неделе я забыла распечатать себе шаблон плана, который я использовала для вписывания туда (от руки) рецептов блюд планируемого меню. От руки рисовать табличку и вписывать туда нужное я просто не могу - живущий во мне заскорузлый перфекционист с ADD-симптоматикой не даёт мне делать что-либо, если не доведена до максимально возможного совершенства рутина выполнения этого “что-либо”. Поэтому я решила, что мне необходимо перевести планирование в полностью электронный формат.
Погуглив, я нашла прекрасный планнер: Plan To Eat. А найдя - прониклась и даже оплачивала несколько месяцев его использование. Но при том, что у этого сервиса - один из самых удобных календарей для планирования, пользоваться им для меня оказалось нереально. Потому что планнер - там, а мои рецепты - тут, в бумажных журналах! Добавлять рецепты с сайтов туда тоже приходилось вручную, поскольку импортировать автоматически PTE может только с иностранных (читай: англоязычных) сайтов. А без базы рецептов или только с рецептами с сайтов для меня такой планнер теряет всякий смысл.
По этой же причине я не смогла пользоваться ни MacGourmet, который я как-то купила в составе какого-то бандла, ни прекрасной программой для айфона Paprika Recipe Manager, которая мало того, что имеет планнер, так ещё и понимает формат MacGourmet - казалось бы, идеальное сочетание! Но проблема оставалась прежней - вручную добавлять рецепты я не могу, журналы пылятся на полках, семья ест макароны и картофельное пюре.
Слушаю любимые версии свинговых баянов. Столько раз слышала - и до сих пор от каждого трека мурашки по телу!

В ходе использования GetSubs обратила внимание на то, что некоторые субтитры не находятся либо находятся неправильно и скачиваются неоднократно, хотя файл с таким же именем уже существует. Проверила логику скрипта и нашла ошибку, связанную с тем, что в массивы с датами публикации субтитров попадали данные о предыдущих обработанных сериалах, и такой файл мог скачиваться несколько раз под разными именами. Теперь ошибка исправлена, массивы обнуляются и подобные ситуации не повторяются :)
Вызываем локальный файл с имеющимися ID. Получаем название сериала из имени файла.
cd /Users/jinjiru/Downloads/Series/
touch /Users/jinjiru/Applications/getsubs.conf
for i in *.avi;
do
. /Users/jinjiru/Applications/getsubs.conf
BN=${i%.*}
showName=${BN%-*}
showNameN=${showName// /220}
showNameN=`echo $showNameN | awk 'sub("...$", "")'`
seasonEpisode=${BN#*-}
seasonNumber=`echo $seasonEpisode | cut -c '2-3'`
episodeNumber=`echo $seasonEpisode | cut -c '5-6'`
localName=$showNameN
localID=${!localName}
Проверяем, есть ли локальный ID для данного сериала, если находим, используем его, если нет - ищем на Bierdopje
if [[ -n $localID ]]; then
showid=$localID
else
showName=${showName// /%20}
showName=`echo $showName | awk 'sub("...$", "")'`
showName=`echo $showName | sed 's/and/\&/g'`
showName=`echo $showName | sed 's/2009/(2009)/g'`
showName=`echo $showName | sed 's/2010/(2010)/g'`
/usr/bin/curl http://api.bierdopje.com/YOUR_USER_API/GetShowByName/$showName > input.xml
showid=`xpath input.xml "//response[position() = 1]/showid/text()" 2>/dev/null`
if [[ -n "$showid" ]]; then
echo "$localName=$showid" >> /Users/jinjiru/Applications/getsubs.conf
fi
fi
Получаем XML для нужного эпизода, с помощью XPath опеределяем количество файлов с субтитрами, доступных для данной серии
/usr/bin/curl http://api.bierdopje.com/YOUR_USER_API/GetAllSubsFor/$showid/$seasonNumber/$episodeNumber/en > input.xml count=$(xpath input.xml "count(//result)" 2>/dev/null)
Для каждого из файлов с титрами определяем ссылку для скачивания. Отсеиваем те файлы, в названии которых есть указание 720p, для всех остальных определяем дату публикации (самые актуальные и отредактированные субтитры обычно имеют самую позднюю дату публикации) и записываем эту дату в массив. Также определяем размер файлов для дальнейшего сравнения, если файл с субтитрами уже есть в папке
startcount=1
while [ $startcount -le $count ]
do
num=$startcount
downloadlink=`xpath input.xml "//result[position() = $startcount]/downloadlink/text()" 2>/dev/null`
if [[ $downloadlink != *720p* ]]; then
link=downloadlink_${num}
eval ${link}="$downloadlink"
pubdate=`xpath input.xml "//result[position() = $startcount]/pubdate/text()" 2>/dev/null`
pubdate="$pubdate"_res"$num"
array=( ${array[@]-} $(echo "$pubdate") )
filesize=`xpath input.xml "//result[position() = $startcount]/filesize/text()" 2>/dev/null`
remotesize=filesize_${num}
eval ${remotesize}="$filesize"
fi
startcount=$((startcount+1))
done
Сортируем полученный массив, получаем таймстамп самого нового файла, опеределяем с его помощью, какую именно ссылку на скачивание файла будем использовать. После этого обнуляем массивы (это важно).
function mysort { for i in ${unsorted_array[@]}; do echo "$i"; done | sort -n; }
unsorted_array=${array[@]}
sorted_array=( $(mysort) )
last_item=( "${sorted_array[@]: -1}" )
subnumber=$(echo -n "$last_item" | tail -c -1)
link1=downloadlink_"$subnumber"
finallink=${!link1}
unset array
unset unsorted_array
unset sorted_array
Если ссылка найдена, проверяем, есть ли уже в папке субтитры для данного эпизода. Если субтитров нет, скачиваем файл по ссылке. Если локальный файл с титрами есть, определяем его размер в байтах и сравниваем с полученным ранее размером удалённого файла. В случае, если размеры различаются, удаляем имеющийся файл и скачиваем его заново.
if [[ -n $finallink ]]; then
# Get the filename so we could use it as -O argument for wget
parts=(${finallink//\// });
fileName="${parts[5]}";
#If there's no .srt file, let's download one
if [ ! -f "$BN.srt" ]; then
`/usr/local/bin/wget -c -O "$fileName".srt "$finallink" &`;
# If there's an .srt file, let's check the size and download only if the size differs
else
remoteSubsFileSize=filesize_"$subnumber"
remoteFileSize=${!remoteSubsFileSize}
localSubsFile=$BN.srt
localSubsFileSize=`/bin/ls -l "$localSubsFile" | /usr/bin/awk '{ print $5 }'`
if [ "$localSubsFileSize" != $remoteFileSize ] ; then
rm "$localSubsFile"
# Now we have downloadLink and fileName, let's download the file
`/usr/local/bin/wget -c -O "$fileName".srt "$finallink" &`;
fi
fi
fi
rm input.xml
unset BN
done
exit 0
Архив с инсталлером, полным набором Automator workflows, а также правилами Hazel доступен по прежнему адресу: Скачать Lazy Series Addict Workflow v0.6

Читая блог Coding Horror, я наткнулась на запись полуторагодичной давности Parsing Html The Cthulhu Way. В ней русским по-белому :) говорится о том, что парсить HTML с помощью регулярных выражений - это категорическое no-no.
Every time you attempt to parse HTML with regular expressions, the unholy child weeps the blood of virgins, and Russian hackers pwn your webapp.
Откровенно говоря, предыдущие версии скрипта Getsubs именно это и делали - парсили xml-файл, получаемый от Bierdopje, с помощью регулярных выражений и утилит sed и awk. Поскольку я не волшебник программист, а только учусь, то выбор инструментов я не всегда делаю оптимальный.
Но надо исправляться :) Поэтому Getsubs снова переписан, на этот раз - практически на 100%. XML парсится с помощью XPath, что позволяет сократить количество запросов к Bierdopje (и облегчить нагрузку на их API), а также уменьшить время, требуемое на обработку xml.
Вызываем локальный файл с имеющимися ID. Получаем название сериала из имени файла.
cd /Users/jinjiru/Downloads/Series/
touch /Users/jinjiru/Applications/getsubs.conf
for i in *.avi;
do
. /Users/jinjiru/Applications/getsubs.conf
BN=${i%.*}
showName=${BN%-*}
showNameN=${showName// /220}
showNameN=`echo $showNameN | awk 'sub("...$", "")'`
seasonEpisode=${BN#*-}
seasonNumber=`echo $seasonEpisode | cut -c '2-3'`
episodeNumber=`echo $seasonEpisode | cut -c '5-6'`
localName=$showNameN
localID=${!localName}
Проверяем, есть ли локальный ID для данного сериала, если находим, используем его, если нет - ищем на Bierdopje
if [[ -n $localID ]]; then
showid=$localID
else
showName=${showName// /%20}
showName=`echo $showName | awk 'sub("...$", "")'`
showName=`echo $showName | sed 's/and/\&/g'`
showName=`echo $showName | sed 's/2009/(2009)/g'`
showName=`echo $showName | sed 's/2010/(2010)/g'`
/usr/bin/curl http://api.bierdopje.com/YOUR_USER_API/GetShowByName/$showName > input.xml
showid=`xpath input.xml "//response[position() = 1]/showid/text()" 2>/dev/null`
if [[ -n "$showid" ]]; then
echo "$localName=$showid" >> /Users/jinjiru/Applications/getsubs.conf
fi
fi
Получаем XML для нужного эпизода, с помощью XPath опеределяем количество файлов с субтитрами, доступных для данной серии
/usr/bin/curl http://api.bierdopje.com/YOUR_USER_API/GetAllSubsFor/$showid/$seasonNumber/$episodeNumber/en > input.xml count=$(xpath input.xml "count(//result)" 2>/dev/null)
Для каждого из файлов с титрами определяем ссылку для скачивания. Отсеиваем те файлы, в названии которых есть указание 720p, для всех остальных определяем дату публикации (самые актуальные и отредактированные субтитры обычно имеют самую позднюю дату публикации) и записываем эту дату в массив. Также определяем размер файлов для дальнейшего сравнения, если файл с субтитрами уже есть в папке
startcount=1
while [ $startcount -le $count ]
do
num=$startcount
downloadlink=`xpath input.xml "//result[position() = $startcount]/downloadlink/text()" 2>/dev/null`
if [[ $downloadlink != *720p* ]]; then
pubdate=`xpath input.xml "//result[position() = $startcount]/pubdate/text()" 2>/dev/null`
pubdate="$pubdate"_res"$num"
array=( ${array[@]-} $(echo "$pubdate") )
link=downloadlink_${num}
eval ${link}="$downloadlink"
filesize=`xpath input.xml "//result[position() = $startcount]/filesize/text()" 2>/dev/null`
remotesize=filesize_${num}
eval ${remotesize}="$filesize"
fi
startcount=$((startcount+1))
done
Сортируем полученный массив, получаем таймстамп самого нового файла, опеределяем с его помощью, какую именно ссылку на скачивание файла будем использовать.
function mysort { for i in ${unsorted_array[@]}; do echo "$i"; done | sort -n; }
unsorted_array=${array[@]}
sorted_array=( $(mysort) )
last_item=( "${sorted_array[@]: -1}" )
subnumber=$(echo -n "$last_item" | tail -c -1)
link1=downloadlink_"$subnumber"
finallink=${!link1}
Если ссылка найдена, проверяем, есть ли уже в папке субтитры для данного эпизода. Если субтитров нет, скачиваем файл по ссылке. Если локальный файл с титрами есть, определяем его размер в байтах и сравниваем с полученным ранее размером удалённого файла. В случае, если размеры различаются, удаляем имеющийся файл и скачиваем его заново.
if [[ -n $finallink ]]; then
parts=(${finallink//\// })
fileName="${parts[5]}"
if [ ! -f "$BN.srt" ]; then
`/usr/local/bin/wget -c -O "$fileName".srt "$finallink" &`
else
remoteSubsFileSize=filesize_"$subnumber"
remoteFileSize=${!remoteSubsFileSize}
localSubsFile=$BN.srt
localSubsFileSize=`/bin/ls -l "$localSubsFile" | /usr/bin/awk '{ print $5 }'`
if [ "$localSubsFileSize" != $remoteFileSize ] ; then
rm "$localSubsFile"
`/usr/local/bin/wget -c -O "$fileName".srt "$finallink" &`
fi
fi
fi
rm input.xml
done
exit 0
Архив с инсталлером, полным набором Automator workflows, а также правилами Hazel доступен по прежнему адресу: Скачать Lazy Series Addict Workflow v0.5

Поскольку на Bierdopje немного ужесточили правила использования API, я всё-таки собрала волю в кулак и обновила скрипт Getsubs, существенно пересмотрев логику процесса.
Те Show ID, что выдаёт сериалам Bierdopje, постоянны, поэтому каждый раз запрашивать их через API-вызов нелогично. Если мы знаем ShowID, проще хранить его локально в конфигурационном файле, а затем при необходимости использовать в запросе субтитров к конкретному эпизоду.
Сказано - сделано: при запуске скрипта проверяем, на месте ли конфигурационный файл, и если нет - создаём его.
touch ~/Applications/getsubs.conf
Вызываем конфиг в начале обработки файла. Получив, как и прежде, название сериала, проверяем, есть ли в конфигурационном файле нужный ID к нему:
for i in *.avi;
do
. ~/Applications/getsubs.conf
BN=${i%.*};
showName=${BN%-*};
showNameN=${showName// /220};
showNameN=`echo $showNameN | awk 'sub("...$", "")'`
localName=$showNameN
localID=${!localName}
Если локальный ID найден (то есть переменная $localID установлена), используем её значение в переменной $showid, которая в дальнейшем будет подставлена в API-вызов. Если локальный ID не найден, ищем его прежним методом на Bierdopje, и если находим, добавляем его в конфигурационный файл:
if [[ -n $localID ]]; then
showid=$localID
else
showName=${showName// /%20};
showName=`echo $showName | awk 'sub("...$", "")'`
showName=`echo $showName | sed 's/and/\&/g'`;
showName=`echo $showName | sed 's/2009/(2009)/g'`;
showName=`echo $showName | sed 's/2010/(2010)/g'`;
showid=$(/usr/bin/curl http://api.bierdopje.com/3C638475DB2F19BE/GetShowByName/$showName | sed -n 's|\(.*\) |\1|p');
showid=`echo $showid | sed 's/^[ \t]*//'`;
if [[ -n "$showid" ]]; then
echo "$localName=$showid" >> ~/Applications/getsubs.conf
fi
fi
Архив с инсталлером, полным набором Automator workflows, а также правилами Hazel доступен по прежнему адресу: Скачать Lazy Series Addict Workflow v0.4
Сегодня, когда подкосивший меня грипп немного отступил, вернулась к приятному делу :) и внесла очередные изменения как в инсталлер, так и в скрипт GetSubtitles.
GetSubtitles ранее прежде всего проверял, есть ли к данному видео-файлу соответствующий .srt-файл, и если находил его, то просто переходил к следующему видео-файлу. Однако возможны случаи, когда по той или иной причине при скачивании субтитров произошёл сбой (сервер перестал отвечать, wget выпал с ошибкой и т.п.), в таком случае имеющийся .srt-файл может быть битым, использовать его нет смысла. Я изменила логику скрипта: теперь скрипт не проводит проверку наличия .srt-файла в начале. Теперь после того, как получена ссылка на субтитры, скрипт проверяет размер .srt-файла на сервере и сравнивает его с тем, что имеется локально. Если размер совпадает, новый файл не закачивается. Если размер не совпадает, то имеющийся файл удаляется, а новый с сервера скачивается.
#Проверяем, получили ли мы ссылку на субтитры
if [[ -n $downloadLink ]]; then
#Если .srt файла нет, скачиваем его с сервера
if [ ! -f "$BN.srt" ]; then
# Получаем имя файла, чтобы использовать его в качестве аргумента для wget
parts=(${downloadLink//\// });
fileName="${parts[5]}";
# Скачиваем
`/usr/local/bin/wget -c -O "$fileName".srt "$downloadLink" &`;
# Если .srt файл есть,...
else
#... проверяем его размер и сравниваем с удалённым.
remoteSubsFileSize=`/usr/bin/curl -sI $downloadLink | grep Content-Length | cut -c17- | sed 's/.\{1\}$//'`
localSubsFile=$BN.srt
localSubsFileSize=`/bin/ls -l "$localSubsFile" | /usr/bin/awk '{ print $5 }'`
#Если размер отличается, удаляем имеющийся файл и скачиваем с сервера новый
if [ "$localSubsFileSize" != $remoteSubsFileSize ] ; then
rm "$localSubsFile"
parts=(${downloadLink//\// });
fileName="${parts[5]}";
`/usr/local/bin/wget -c -O "$fileName".srt "$downloadLink" &`;
fi
fi
fi
Проблемой же инсталлера было то, что во время приаттачивания dmg-образа с воркфлоу Автоматора он не учитывал, что “двойной клик на скрипте” не означает, что скрипт знает, из какой конкретно папки он запущен. А поскольку путь к образу использовался относительный, то при подключении образа возникала ошибка - образ не мог быть найден. Теперь сие недоразумение исправлено: скрипт сам проверяет, из какой папки запущен, и использует соответствующий путь к образу:
currentDir=$(cd `dirname $0` && pwd) cp -R $currentDir/Automator\ Workflows/* ~/Library/Services/ hdiutil attach "$currentDir/BatchRipActions-1.0.4.dmg"
Также в прошлой версии инсталлер, хотя и спрашивал о том, будете ли вы вообще использовать субтитры, и соответственно вашему ответу либо вносил, либо не вносил в скрипт-конвертер соответствующую проверку на наличие субтитров, в пресетах всё равно использовалось указание на файл с титрами и его формат. Теперь же если на вопрос инсталлера, будут ли вообще использоваться субтитры для конвертации, ответить отрицательно, то помимо удаления проверки на наличие субтитров инсталлер также удалит любое упоминание о субтитрах из пресетов HandBrake CLI, используемых согласно выбранному вами устройству.
Архив с инсталлером, полным набором Automator’s workflows, а также правилами Hazel доступен по прежнему адресу: Скачать Lazy Series Addict Workflow v0.3
Судя по тому, что никто не сообщил мне о небольшой ошибке в скрипте-инсталлере, никто им и не пользовался :)
Тем не менее, я внесла в него небольшие изменения, исправила некоторые баги (и, разумеется, добавила немного новых ;)
Архив доступен по прежнему адресу: Скачать Lazy Series Addict Workflow v0.2
Lazy Series Addict Workflow Changelog:
Я уже давно не выключаю компьютер на ночь. В ночное время происходит много всего интересного - скачиваются торренты, проводятся профилактические работы, а также запускаются несколько UIScript’ов, написанных на Sikuli IDE. Однако недавно любимый мужчина сказал, что совершенно не может спать под звук “этих реактивных двигателей”. И действительно, кулеры в моём Mac Mini ощутимо шумят, так как разогнаны с помощью smcFanControl до 3600RPM. Подобный разгон необходим для того, чтобы при перекодировании видео компьютер не перегревался - на стандартных установках температура при кодировании фильмов достигала 80-90 градусов.
Но выключать компьютер я не готова :) Хорошо бы сделать так, чтобы и волки были целы, и овцы сыты :) То есть чтобы ночью было тихо, а в нужные моменты (например, днём во время кодирования видео) кулеры разгонялись быстрее. Вот этим и займёмся.
Что необходимо получить в итоге?