Просматривая 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 за рефакторинг парсера, выглядит просто замечательно, последнюю версию смотрите на гитхабе