Как стать автором
Обновить

Пишем Anime Wallpaper Downloader на MacRuby

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

Теги:
Хабы:
+14
Комментарии30

Публикации

Истории

Работа

Ruby on Rails
11 вакансий
Программист Ruby
8 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн