aboutsummaryrefslogtreecommitdiffstats
path: root/core/plugin/openimg/model/photo.rb
blob: 69c1ad43a4c99a211dad6b539ecc60c8063cedb2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# -*- coding: utf-8 -*-

module Plugin::Openimg
  class Photo < Retriever::Model
    register :openimg_photo, name: Plugin[:openimg]._('画像ビューア')

    field.uri    :perma_link
    field.string :blob

    handle ->uri{
      uri_str = uri.to_s
      openers = Plugin.filtering(:openimg_image_openers, Set.new).first
      openers.any?{ |opener| opener.condition === uri_str } if !openers.empty?
    } do |uri|
      new(perma_link: uri)
    end

    # 画像をダウンロードする。
    # partialを指定すると、ダウンロードの進捗があれば、前回呼び出されたときから
    # ダウンロードできた内容を引数に呼び出される。
    # 既にダウンロードが終わっていれば、 _blob_ の戻り値がそのまま渡される。
    # このメソッドは、複数回呼び出されても画像のダウンロードを一度しか行わない。
    # ==== Args
    # [&partial_callback] 現在ダウンロードできたデータの一部(String)
    # ==== Return
    # [Delayer::Deferred::Deferredable] ダウンロードが完了したらselfを引数に呼び出される
    def download(&partial_callback)      # :yield: part
      case @state
      when :complete
        partial_callback.(blob) if block_given?
        Delayer::Deferred.new.next{ self }
      when :download
        append_download_queue(&partial_callback)
      else
        download!(&partial_callback)
      end
    end

    # 画像のダウンロードが終わっていれば真を返す。
    # 真を返す時、 _blob_ には完全な画像の情報が存在している
    def completed?
      @state == :complete
    end

    # 画像をダウロード中なら真
    def downloading?
      @state == :download
    end

    # ダウンロードが始まっていなければ真
    def ready?
      !@state
    end

    private

    def download!(&partial_callback)
      atomic do
        return download(&partial_callback) unless ready?
        promise = initialize_download(&partial_callback)
        Thread.new(&gen_download_routine).next{|success|
          if success
            finalize_download_as_success
          else
            Delayer::Deferred.fail false
          end
        }.trap{|exception|
          finalize_download_as_fail(exception)
        }.terminate('error')
        promise
      end
    end

    def append_download_queue(&partial_callback)
      atomic do
        return download(&partial_callback) unless downloading?
        register_partial_callback(partial_callback)
        register_promise
      end
    end

    def register_promise
      promise = Delayer::Deferred.new(true)
      (@promises ||= Set.new) << promise
      promise
    end

    def register_partial_callback(cb)
      @partials ||= Set.new
      if cb
        @partials  << cb
        cb.(@buffer) if !@buffer.empty?
      end
    end

    def gen_download_routine
      -> do
        begin
          _, raw = Plugin.filtering(:openimg_raw_image_from_display_url, perma_link.to_s, nil)
          if raw
            download_mainloop(raw)
          else
            raise "couldn't resolve actual image url of #{perma_link}."
          end
        rescue EOFError
          true
        ensure
          raw.close rescue nil
        end
      end
    end

    def download_mainloop(raw)
      loop do
        Thread.pass
        partial = raw.readpartial(1024**2).freeze
        @buffer << partial
        atomic{ @partials.each{|c|c.(partial)} }
      end
    end

    def initialize_download(&partial_callback)
      @state = :download
      @buffer = String.new
      register_partial_callback(partial_callback)
      register_promise
    end

    def finalize_download_as_success
      atomic do
        self.blob = @buffer.freeze
        @state = :complete
        @promises.each{|p| p.call(self) }
        @buffer = @promises = @partials = nil
      end
    end

    def finalize_download_as_fail(exception)
      atomic do
        @state = nil
        @promises.each{|p| p.fail(exception) }
        @buffer = @promises = @partials = nil
      end
    end
  end
end