Просматривая theotaku.com в поисках интересных обоев для рабочего стола я поймал себя на мысли о том, что неплохо бы написать софт который по тэгам сам автоматически скачивал бы обои вместо меня. Исходя из того что я пользуюcь Мac OS X как основной операционной системой, софт тоже должен быть для этой платформы и желательно иметь Cocoa интерфейс. Писать всё это на Java почему-то не захотелось. Альтернатив конечно было много, но почему-то захотелось попробовать чего-то другого и заодно научится чем-то новому. Сразу же вспомнил о MacRuby и его тесной интеграции с Cocoa. Вооружившись этой идеей, я сразу же полез на http://www.macruby.org/ и скачал последнюю стабильную версию 0.10. После установки я запустил любимый XCode и создал новый проект с названием AnimeWallpaperDownloader
Наш проект на MacRuby состоит из нескольких файлов, которые XCode создаёт за нас. Первый файл это main.m который просто запускает MacRuby скрипт rb_main.rb
rb_main.rb довольно простой скрипт, он подгружает все остальные Ruby скрипты и запускает
NSApplicationMain
Последний файл который за нас уже создан это AppDelegate.rb, который играет роль NSApplicationDelegate. Он содержит пустой метод applicationDidFinishLaunching который вызывается когда наша программа закончила запускаться.
Тут attr_accessor :window играет роль IBOutlet *window и уже привязан к окну нашей программы
Открываем MainMenu.xib и создаём простой интерфейс для нашего wallpaper downloader-а
Далее добавляем методы и outlet-ы в наш AppDelegate
Тут всё довольно просто. Методы windowWillClose и controlTextDidChange просто методы делегатов для окна программы и первого текстового поля (пока не впишем тэг ничего скачивать нельзя).
Метод browse открывает диалоговое окно для выбора директории куда мы сохраняем наши обои, его мы привязываем к кнопке Browse. Метод startStop запускает скачку, так что его мы привязываем к кнопке Start Download. Остальные методы вспомогательные и будут использоваться классом Downloader, который мы будем использовать для нахождения ссылок и скачивания обоев
Огромный класс неправда ли? Да он нашпигован логикой, но на самом деле всё достаточно тривиально.
Метод start просто запускает отдельный поток который и будет скачивать наши обои, stop его останавливает. Методы getIndexPage, getWallUrl, downloadWall cпецифичны для сайта theotaku.com и содержат достаточно много логики, но по сути достаточно тривиальны и используются для поиска обоeв по тэгу, подбора нужной ссылки на желательный размер обоев и скачивания этих обоев
Итог потраченный воскресный вечер, неплохое чувство самоудовлетворения, а также много интересных мыслей о будущем МacRuby как альтернативной платформы для разработки на Mac OS X. Конечно же без граблей не обошлось и некоторые вещи не получилось сделать, но я думаю что платформа MacRuby начинает набирать популярность и у неё светлое будущее.
Результат можно посмотреть вот тут, ну а готовый билд можно скачать вот отсюда (у вас должен быть установлен MacRuby)
Cпасибо за внимание
bye-nii
UPD: Cпасибо Aesthete за рефакторинг парсера, выглядит просто замечательно, последнюю версию смотрите на гитхабе
Наш проект на MacRuby состоит из нескольких файлов, которые XCode создаёт за нас. Первый файл это main.m который просто запускает MacRuby скрипт rb_main.rb
#import <Cocoa/Cocoa.h>
#import <MacRuby/MacRuby.h>
int main(int argc, char *argv[])
{
return macruby_main("rb_main.rb", argc, argv);
}
rb_main.rb довольно простой скрипт, он подгружает все остальные Ruby скрипты и запускает
NSApplicationMain
framework 'Cocoa'
# Loading all the Ruby project files.
main = File.basename(__FILE__, File.extname(__FILE__))
dir_path = NSBundle.mainBundle.resourcePath.fileSystemRepresentation
Dir.glob(File.join(dir_path, '*.{rb,rbo}')).map { |x| File.basename(x, File.extname(x)) }.uniq.each do |path|
if path != main
require(path)
end
end
# Starting the Cocoa main loop.
NSApplicationMain(0, nil)
Последний файл который за нас уже создан это AppDelegate.rb, который играет роль NSApplicationDelegate. Он содержит пустой метод applicationDidFinishLaunching который вызывается когда наша программа закончила запускаться.
class AppDelegate
attr_accessor :window
def applicationDidFinishLaunching(a_notification)
# Insert code here to initialize your application
end
end
Тут attr_accessor :window играет роль IBOutlet *window и уже привязан к окну нашей программы
Открываем MainMenu.xib и создаём простой интерфейс для нашего wallpaper downloader-а
Далее добавляем методы и outlet-ы в наш AppDelegate
class AppDelegate
attr_accessor :window
attr_accessor :tags
attr_accessor :size
attr_accessor :number
attr_accessor :saveInto
attr_accessor :startButton
attr_accessor :output
attr_accessor :downprogress
attr_accessor :downloader
attr_accessor :img
def applicationDidFinishLaunching(a_notification)
@startButton.setEnabled(false)
@downprogress.setStringValue('')
@output.setStringValue('')
@saveInto.stringValue = NSHomeDirectory()+"/Pictures"
end
def windowWillClose(a_notification)
NSApp.terminate(self)
end
def controlTextDidChange(notification)
sender = notification.object
if sender == tags
@startButton.setEnabled(@tags.stringValue.size > 0)
elsif sender == number
begin
@number.setIntValue(@number.intValue)
if @number.intValue < 0
@number.setIntValue(-@number.intValue)
elsif @number.intValue == 0
@number.setIntValue(20)
end
rescue
@number.setIntValue(20)
end
end
end
def browse(sender)
dialog = NSOpenPanel.openPanel
dialog.canChooseFiles = false
dialog.canChooseDirectories = true
dialog.allowsMultipleSelection = false
if dialog.runModalForDirectory(nil, file:nil) == NSOKButton
@saveInto.stringValue = dialog.filenames.first
end
end
def startStop(sender)
if @downloader == nil
@downloader = Downloader.new(@tags.stringValue,@size.selectedItem.title,@number.stringValue,@saveInto.stringValue,self)
@downloader.start
@startButton.setTitle("Stop Download")
else
@downloader.stop
@downloader = nil
@startButton.setTitle("Start Download")
end
end
def changeImage(file)
@img.setImage(NSImage.alloc.initByReferencingFile(file))
end
def clearStatus
@downprogress.setStringValue('')
end
def setStatus(i,m)
@downprogress.setStringValue("Downloading "+i.to_s()+" of "+m.to_s())
end
def setStatusEnd(i)
@downprogress.setStringValue("Downloaded "+i.to_s()+" wallpapers")
end
def puts(val)
$stdout.puts val
@output.setStringValue(val)
end
def stopped
@startButton.setTitle("Start Download")
down = @downloader
@downloader = nil
down.stop
end
end
Тут всё довольно просто. Методы windowWillClose и controlTextDidChange просто методы делегатов для окна программы и первого текстового поля (пока не впишем тэг ничего скачивать нельзя).
Метод browse открывает диалоговое окно для выбора директории куда мы сохраняем наши обои, его мы привязываем к кнопке Browse. Метод startStop запускает скачку, так что его мы привязываем к кнопке Start Download. Остальные методы вспомогательные и будут использоваться классом Downloader, который мы будем использовать для нахождения ссылок и скачивания обоев
require 'thread'
require 'net/http'
class Downloader
attr_accessor :tags, :size, :number, :saveTo, :thread
attr_accessor :app, :exit
def initialize(tags, size, number, saveTo, app)
@tags = tags.sub(' ','_')
@size = size == 'Any' ? '' : size.sub('x','_')
@number = number
@saveTo = saveTo
@app = app
@exit = false
end
def getIndexPage(page)
walls = {}
url = 'http://www.theotaku.com/wallpapers/tags/'+tags+'/?sort_by=&resolution='+size+'&date_filter=&category=&page='+page.to_s()
@app.puts 'getting index for page: '+page.to_s()
@app.puts url
response = Net::HTTP.get_response(URI.parse(url))
res = response.body
res.each_line { |line|
f = line.index('wallpapers/view')
while f != nil
b = line.rindex('"',f)
e = line.index('"',b+1)
u = line[b+1,e-b].gsub('"','')
walls[u] = u
line = line.sub(u,'')
f = line.index('wallpapers/view')
end
}
@app.puts 'got '+walls.size.to_s()+' wallpapers'
return walls.keys
end
def downloadWall(url)
@app.puts 'downloading '+url
response = Net::HTTP.get_response(URI.parse(url))
res = response.body
b = res.index('src',res.rindex('wall_holder'))+5
e = res.index('"',b)
img = res[b,e-b]
self.downloadFile(img)
end
def downloadFile(url)
name = url[url.rindex('/')+1,1000]
if File.exists?(@saveTo+'/'+name)
@app.puts 'wallpaper already saved '+name
@app.changeImage(@saveTo+'/'+name)
else
@app.puts 'downloading file '+url
response = Net::HTTP.get_response(URI.parse(url))
open(@saveTo+'/'+name, 'wb') { |file|
file.write(response.body)
}
@app.puts 'wallpaper saved '+name
@app.changeImage(@saveTo+'/'+name)
end
end
def getWallUrl(i,url,size)
sizes = {}
i = i+1
@app.puts 'getting '+url+' sizes'
response = Net::HTTP.get_response(URI.parse(url))
res = response.body
res.each_line { |line|
f = line.index('wallpapers/download')
while f != nil
b = line.rindex('\'',f)
e = line.index('\'',b+1)
u = line[b+1,e-b]
u = u.gsub('\'','')
sizes[u] = u
line = line.sub(u,'')
f = line.index('wallpapers/download')
end
}
sizef = @size.sub('_','-by-')
sizes = sizes.keys()
if sizef == ''
maxi = 0
max = 0
i = 0
sizes.each { |s|
f = s.rindex('/')
l = s[f+1,100]
l = l.sub('-by-',' ')
l = l.split(' ')
rs = l[0].to_i()*l[1].to_i()
if rs > max
maxi = i
max = rs
end
i = i+1
}
return sizes[maxi]
else
sizes.each { |s|
if s =~ /#{Regexp.escape(sizef)}$/
return s
end
}
end
return sizes[0]
end
def start
@thread = Thread.new {
@app.puts "Download started"
begin
i = 0
p = 1
@app.clearStatus
while i < @number.to_i() and not @exit
w = self.getIndexPage(p)
if w.size == 0
break
end
w.each { |w|
wallu = self.getWallUrl(i,w,self.size)
if wallu != nil
@app.setStatus(i+1,@number.to_i())
self.downloadWall(wallu)
i = i+1
if i >= @number.to_i() or @exit
break
end
end
}
p = p+1
end
@app.puts ""
@app.setStatusEnd(i)
rescue => e
puts e
end
@app.stopped
}
end
def stop
begin
@app.puts "Download stopped"
if @thread.alive?
if @thread == Thread.current
Thread.exit(0)
else
@exit = true
end
end
rescue => e
puts e
end
end
end
Огромный класс неправда ли? Да он нашпигован логикой, но на самом деле всё достаточно тривиально.
Метод start просто запускает отдельный поток который и будет скачивать наши обои, stop его останавливает. Методы getIndexPage, getWallUrl, downloadWall cпецифичны для сайта theotaku.com и содержат достаточно много логики, но по сути достаточно тривиальны и используются для поиска обоeв по тэгу, подбора нужной ссылки на желательный размер обоев и скачивания этих обоев
Итог потраченный воскресный вечер, неплохое чувство самоудовлетворения, а также много интересных мыслей о будущем МacRuby как альтернативной платформы для разработки на Mac OS X. Конечно же без граблей не обошлось и некоторые вещи не получилось сделать, но я думаю что платформа MacRuby начинает набирать популярность и у неё светлое будущее.
Результат можно посмотреть вот тут, ну а готовый билд можно скачать вот отсюда (у вас должен быть установлен MacRuby)
Cпасибо за внимание
bye-nii
UPD: Cпасибо Aesthete за рефакторинг парсера, выглядит просто замечательно, последнюю версию смотрите на гитхабе