Page 116 of 165

Poprawne linki z Facebooka dla JWPlayera – wygasające linki – część I

Wstęp

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Część z Was na pewno osadza swoje filmiki z konta Facebook także na swoich stronach www. Niektórzy robią to z pomocą odtwarzacza dostarczonego przez Facebooka, inni korzystają np. z JWPlayera. Niestety ostatnimi czasy, Facebook zauważył, że staje się bardzo fajną platformą hostingową dla filmów wszelakich (i to w jakości HD!). Zjadało im to (i zjada ;) ) gigantyczne ilości zasobów - przede wszystkim łącza. Jeśli osadzasz pliki wideo przez ich odtwarzacz - to nie ma problemu - FB ma z tego korzyści (mają swoje logo na filmiku, itd). Korzystając jednak z JWPlayera - nie uda ci się ta sztuczka. Linki bezpośrednie do plików MP4 są zmieniane co 24-36 godzin, w skutek czego umieszczanie ich "w" odtwarzaczu nie ma sensu. Na szczęście da się to bardzo łatwo rozwiązać, jak zawsze w... Rubym :)

Mechanize

Napiszemy prostego bota - który wchodzi na Facebooka, loguje się i sprawdza URL filmu. Warto dodać cacheowanie, tak aby nie odpytywać FB za każdym razem o to samo. Rozbudowę tego narzędzia pozostawię jednak Wam. Wracając do sedna sprawy. Aby napisać tego bota, skorzystamy z Mechanize. Mechanize jest biblioteką stworzoną do łatwej integracji ze stronami wszelakimi. Umożliwia przesyłanie formularzy, odwiedzanie stron, zapewnia obsługę cookies, ssl-a, itp, itd. To głównie dzięki niemu, będziemy mogli odświeżać linki do plików MP4 z FB.

Zanim jednak to zrobimy, musimy dodać do Mechanize pewną małą poprawkę. Domyślnie nie pozwala wyszukiwać formularzy po ID a nam taka metoda się przyda. Tak więc:

class Mechanize::Page
  def form_id(formId)
    formContents = (self/:form).find { |elem| elem['id'] == formId }
    if formContents then return Mechanize::Form.new(formContents) end
  end
end

Tyle :) A teraz pora na FacebookBota.

FacebookBot

Nasz bot będzie miał łącznie 4 metody:

  1. Initialize - inicjalizacja bota
  2. Login - logowanie do Facebooka
  3. Video_Url - pobranie URLa pliku wideo
  4. (priv) get_url - wyodrębnienie samego urla pliku ze strony z linkiem

Szkielet klasy będzie więc wyglądał tak:

require 'rubygems'
require 'mechanize'
require 'uri'
require 'cgi'
require 'time'

class FacebookBot
  # Strona główna Facebooka
  FB_URL = "http://www.facebook.com/"
  # Nasza przeglądarka i system :)
  USER_AGENT = 'Linux Firefox'

  def initialize(email, pass)
  end

  def login
  end

  def video_url(video_id)
  end

  private

  def get_url(url)
  end
end

Inicjalizacja naszego bota składa się z zapamiętania e-maila i hasła, utworzenia obiektu Mechanize do eksploracji FB oraz próby zalogowania na nasze konto:

  def initialize(email, pass)
    @email, @pass = email, pass

    @agent = Mechanize.new
    @agent.user_agent_alias = USER_AGENT

    @cookies = File.dirname(__FILE__) + "/../cookies-" + @email + ".yml"
    if (File.file?(@cookies))
      @agent.cookie_jar.load(@cookies)
    end

    self.login
  end

Wartym omówienia jest ten fragment:

    @cookies = File.dirname(__FILE__) + "/../cookies-" + @email + ".yml"
    if (File.file?(@cookies))
      @agent.cookie_jar.load(@cookies)
    end

Zmienna @cookies trzyma ciastka sesji od Facebooka na dysku. Dlaczego jest o katalog wyżej (../) niż sam plik? Ponieważ plik z botem wrzucimy do 'lib/' a plik z ciastkiem będziemy chcieli mieć w katalogu głównym naszej małej aplikacji. Tak więc, ustalamy sobie ścieżkę, sprawdzamy czy plik już istnieje i jeśli tak jest, to ładujemy jego zawartość do kontenera na ciacho - tak żeby można się było przedstawiać nim w Facebooku. Następnie podejmujemy próbę logowania.

Pora na logowanie:

  def login
    page = @agent.get(FB_URL)

    if (loginf = page.form_id("login_form"))
      loginf.set_fields(:email => @email, :pass => @pass)
      page = @agent.submit(loginf, loginf.buttons.first)
    end

    @agent.cookie_jar.save_as(@cookies)

    body = page.root.to_html
    @uid = %r{\\"user\\":(\d+),\\"hide\\"}.match(body)[1]
    @post_form_id = %r{<input type="hidden" id="post_form_id" name="post_form_id" value="([^"]+)}.match(body)[1]
  end

Logowanie przebiega w następujący sposób:

  1. Próbujemy przejść na stronę główną FB.
  2. Jeśli się to udało - tzn że nasze ciastka były i były prawidłowe
  3. Jeśli nie, to wypełniamy formularz logowania i klikamy na "Zaloguj"
  4. Zapamiętujemy uaktualnione ciastka
  5. Przechodzimy na stronę główną i zapamiętujemy id usera (UID)
  6. Zapamiętujemy klucz do przesyłania żądań typu POST

Prawda że proste? :)

Video_url:

def video_url(id)
    begin
      pa = @agent.get("#{FB_URL}video/video.php?v=#{id}")
      pr = pa.body
      get_url(pr)
    rescue
      "Niepoprawne ID pliku wideo lub wideo nie jest publiczne."
    end
end

Pobieramy stronę pliku wideo, wyciągamy link do MP4 i zwracamy. Jeśli coś pójdzie nie tak, to zwracamy info że wideo jest nieprawidłowe lub nie jest publiczne.

Samo parsowanie przebiega w sposób trochę "brutalny" - nie chciało mi się bawić i myśleć to wyciągnąłem tak niezbyt elegancko:

private

def get_url(url)
	url = url.scan(/addVariable\(\"highqual_src\",\s\"http.+\"\)/ix).first
	url = url.split(')').first.gsub('\u00253A', ':')
	url = url.gsub('\u00252F', '/')
	url = url.gsub('\u00253F', '?')
	url = url.gsub('\u00253D', '=')
	url = url.gsub('\u002526', '&')
	url = "http://#{url.split('http://')[1]}".split('"').first
	CGI.unescapeHTML(url)
end

Grunt że działa.
Oto jak działa całość:

  fb = FacebookBot.new('moj@mail.pl', 'moje_tajne_haslo')
  p  fb.video_url(ID_PLIKU_VIDEO)

Warto dodać do tego system cacheowanie, tak aby nie odpytywać FB za każdym razem o to samo!

Źródełko :)

W związku z szerokim zainteresowaniem, udostępniam prosty interfejs: www.fbc.mensfeld.pl. Linki do odcinków buduje się tak:

www.fbc.mensfeld.pl/ID-ODCINKA.mp4

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Simple Rails RSS Feed Reader – czytnik Feedów RSS

Wstęp

v5 (a jakże ;) ) wyposażone będzie (a w sumie to już jest) w dość miły Feed Reader. Z tego względu, postanowiłem pokazać Wam jak zrobić podobny (tylko skromniejszy) samemu. Co ma nasz czytnik oferować?

  • Możliwość podpinania się pod różne kanały
  • Możliwość filtrowania newsów - wg. słów kluczowych w polu tytułu
  • Ma weryfikować czy link działa (status 200)
  • Możliwość odświeżania wszystkich subskrypcji naraz

W tym mini tutorialu skupię się jedynie na samej idei, tak więc nie będzie ani testów ani niczego "super" zaawansowanego. Będzie to jednak dobra baza do napisania swojego większego. W tutorialu pomijam takie "banałki" jak kod do generowania modeli, itp. Tego jest pełno w innych poradnikach. Zaczynajmy więc :)

Struktura modeli

Najpierw sama struktura modeli. Będziemy mieli subskrypcje (Subscription) oraz same newsy zwane Feedami (Feed). Żeby było fajniej, całość zamkniemy w przestrzeni RSS, tak więc:

  • Rss::Subscription - odpowiada  subskrypcji pojedynczego kanału
  • Rss::Feed - odpowiada pojedynczej wiadomości z kanału

Szkielet subskrypcji wygląda następująco:

require 'rss'
require 'date'

class Rss::Subscription < ActiveRecord::Base
  set_table_name "rss_subscriptions"
  # Reszta kodu którym zajmiemy się za chwilkę :)
end

Pola jakie nasza subskrypcja ma mieć (a które sami dopiszecie do pliku migracji ;) ) to:

  • source - link do źródła feedów
  • refreshed_at - data ostatniej aktualizacji subskrypcji
  • name - nazwa kanału
  • description - opis kanału
  • filter_keywords - opcjonalne słowa kluczowe do filtracji

Pierwszą rzeczą jaką musimy dodać, jest info o tym że nasza subskrypcja może mieć feedy:

  has_many :feeds, :class_name => 'Rss::Feed', :dependent => :destroy

Dalej powinny byc walidacje jak np:

  validates_uniqueness_of :name
  validates_uniqueness_of :source

I tego typu walidacje, które są tak proste, że pozostawię je wam :) - trzeba sprawdzić poprawność URLa źródła, długość nazwy i opisu i ew. słowa kluczowe (jeśli są wymagane).

Metody modelu Rss::Subscription

Zanim przejdziemy dalej i rozbudujemy sobie nasz model, musimy zrobić listę metod które ma obsługiwać, a także stworzyć model pojedynczego Feeda. Tak więc metody modelu Rss::Subscription:

  • self.refresh_all! - odświeża wszystkie kanały sprawdzając czy są nowe wiadomości
  • refresh! - odświeża pojedynczy kanał
  • validate_all! - usuwa stare niedziałające feedy (prowadzące do 404 lub 500 - ogólnie takie które nie zwracają 200)
  • validate! - jw. ale dla pojedynczego kanału
  • private has_keyword?(string) - czy podany string (w naszym wypadku tytuł feeda) zawiera któreś ze słów kluczowych (pod warunkiem, że je sprawdzamy)

Rss::Feed

Przejdźmy do pojedyńczego Rss::Feed'a (szkielecik):

require 'net/http'
require 'digest/sha2'

# Odpowiednik jednego wpisu z RSSa
class Rss::Feed < ActiveRecord::Base
  set_table_name "rss_feeds"

  # Reszta kodu do obsługi wpisu
end

Poinformujmy Railsy, że ten model do kogoś należy (do Rss::Subscription) i dodajmy scope'y:

  default_scope order("published_at DESC")
  scope :recent, joins(:subscription).limit(8)
  belongs_to :subscription, :class_name => 'Rss::Subscription'

A oto jakie pola zawiera model Feeda:

  • subscription_id - klucz obcy
  • hash - hash do sprawdzania unikalności wpisu
  • title - tytuł
  • description - opis
  • address - adres zwrotny z feeda - do otwarcia danej wiadomości na portalu źródła
  • published_at - data publikacji na źródle

Metod które ma Feed za dużo nie będzie:

  • working? - czy adres zwrotny jest poprawny
  • private create_hash - tworzy nowy unikatowy hash - potrzebne aby nasz czytnik nie tworzył wpisów które już mamy w systemie.

Najpierw zajmijmy się metodą working?:

  # Czy dany wpis jest aktywny na serwerze (czy status 200)
  def working?
    begin
      case Net::HTTP.get_response(URI.parse(self.address))
      when Net::HTTPSuccess then true
      else false
      end
    rescue
      false
    end
  end

Sprawdza ona czy odpowiedź z adresu jest poprawna (Success czyli 200). Jeśli tak, to zwraca true, jeśli nie to false.

Do zrealizowania mamy jeszcze prywatną metodę create_hash która tworzy nam hash weryfikujący unikalność danego feeda - w obrębie danej subskrypcji. Możnaby zapytać, dlaczego nie weryfikujemy tylko daty wpisu - przecież nie zdarza się za często, że dwa wpisy są o tej samej porze. Fakt - nie zdarza się za często w obrębie jednej subskrypcji, ale zdarzyć się może w obrębie wielu.

  private

  # Tworzy hash do sprawdzania unikalnosci danego feeda
  def create_hash
    self.hash = Digest::SHA2.hexdigest("#{self.published_at}#{self.subscription_id}")
  end

Hash składa się z daty publikacji oraz klucza obcego, przez co weryfikacja unikalności odbywa się w obrębie danej subskrypcji. Można "dorzucić" jeszcze tytuł, jednak tytuł może ulec zmianie a wtedy hash ulegnie zmianie, przez co otrzymamy dwa razy ten sam wpis. Można by było weryfikować także adres, jednak w wypadku reorganizacji serwisu - adresy mogłyby się po pewnym czasie "nakładać" a feedy nie byłyby dodawane.

Informujemy Railsy, że przed walidacją warto stworzyć hash oraz że ma być on unikatowy:

  before_validation :create_hash
  validates_uniqueness_of :hash

i to by było na tyle jeśli chodzi o Rss::Feed.

Dokończenie modelu Rss::Subscription

Teraz już będzie z górki. Najpierw metoda klasowa validate_all!:

  # Usuwa niedziałające feedy ze wszystkich kanałów
  def self.validate_all!
    self.all.each do |el|
      el.validate!
    end
  end

Oraz jej wersja dla danej instacji (co kolejno wywołujemy wyżej):

  # Usuwa niedziałające feedy (takie których link nie działa)
  def validate!
    self.feeds.each do |feed|
      feed.destroy unless feed.working?
    end
  end

Warto pamiętać, że jeśli nie będzie dostępu do internetu, to linki także zostaną uznane za niedziałające i zostaną usunięte. Warto np. wprowadzić dwustopniowe usuwanie - jeśli link nie działa - to jest on oznaczany jako nieaktywny - i sprawdzany w późniejszym okresie po raz 2. Ale to pozostawiam wam :)

Odświeżenie wszystkich subskrypcji:

  # Aktualizacja wszystkich subskrypcji
  def self.refresh_all!
    self.all.each do |el|
      el.refresh!
    end
  end

Sprawdzanie słów kluczowych:

  private

  def has_keyword?(string)
    return true if self.filter_keywords.nil? || self.filter_keywords.length == 0

    keywords = self.filter_keywords.downcase.split(',')
    downcase_title = string.downcase
    keywords.each do |keyword|
      return true if downcase_title.include?(keyword.strip)
    end

    return false
  end

I najprzyjemniejsze ^^, kod odświeżający pojedynczą subskrypcję:

  # Odswiezamy kanał RSS
  def refresh!
    self.refreshed_at = Time.now.to_s(:db)
    # Jesli odswiezamy instancję - to musi być ona poprawna
    # W zasadzie korzystałem z tego w czasie testów, ale zostawiłem tak na zapas ;)
    return unless self.valid?

    # Próbujemy otworzyć kanał RSS i pobrać feedy
    begin
      new_feeds = RSS::Parser.parse(open(self.source).read, false).items
    rescue
      new_feeds = []
    end

    # Spróbuj przeparsować kolejne wpisy - jeśli się nam nie udało - to nie ma wpisów
    # i nic nie zostanie przeparsowane
    new_feeds.each do |f|
      begin
        title = f.title.to_s
        published_at = DateTime.parse(f.pubDate.to_s).to_s(:db)
        address = f.link.to_s
        description = f.description.to_s
        # Przejdz do nastepnego feeda ten nam nie pasuje do słów kluczowych
        next unless has_keyword?(title)
        # Jesli takiego feeda nie było - to go zapisz
        data = {:title => title, :published_at => published_at, :address => address, :description => description}
        new_feed_record = self.feeds.new(data)
        new_feed_record.save! if new_feed_record.valid? && new_feed_record.working?
      rescue
        # Jeżeli coś poszło nie tak - przeskocz do następnego feeda
        next
      end
    end
  end

No i to by było na tyle! Warto przetestować na koniec :) :

sub = Rss::Subscription.new
rss.title = "Dev blog"
rss.source = 'http://dev.mensfeld.pl/feed/'
rss.refresh!
rss.recent.each {|feed| p feed.title }

Źródła

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑