diff --git a/bot/bot.go b/bot/bot.go
index 7d55774e81d57449fc62c8950ba6ddfe8dc3e8aa..c378019f83041db8598355ec3971f51ccd43ddc0 100644
--- a/bot/bot.go
+++ b/bot/bot.go
@@ -2,31 +2,53 @@ package bot
 
 import (
 	"log"
+	"regexp"
 	"sync"
 
 	"github.com/mvdan/xurls"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Bot interface {
 	Collection
 
-	ParseLine(author string, line string, contents chan Content) error
+	ParseLine(author string, line string, contents chan *site.Content) error
 }
 
 type PlayBot struct {
 	Collection
 }
 
-func NewPlayBot(source string, db Db) *PlayBot {
+func NewPlayBot(source string, db Db, readers []site.Reader) *PlayBot {
 	return &PlayBot{
 		Collection: &PlayBotCollection{
-			Source: source,
-			Db:     db,
+			Source:  source,
+			Db:      db,
+			Readers: readers,
 		},
 	}
 }
 
-func (pb *PlayBot) ParseLine(author string, line string, contents chan Content) error {
+func (*PlayBot) ExtractTags(line string) []string {
+	tagsSeen := make(map[string]bool)
+	var tags []string
+	re := regexp.MustCompile(`(?:^| )#([a-zA-Z0-9_]+)`)
+
+	for _, match := range re.FindAllStringSubmatch(line, -1) {
+		if tagsSeen[match[1]] {
+			continue
+		}
+
+		tagsSeen[match[1]] = true
+		tags = append(tags, match[1])
+	}
+
+	return tags
+}
+
+func (pb *PlayBot) ParseLine(author string, line string, contents chan *site.Content) error {
+	tags := pb.ExtractTags(line)
 	urls := xurls.Strict.FindAllString(line, -1)
 	var wg sync.WaitGroup
 
@@ -35,7 +57,7 @@ func (pb *PlayBot) ParseLine(author string, line string, contents chan Content)
 		go func(url string) {
 			defer wg.Done()
 
-			content, err := pb.Add(url)
+			content, err := pb.Add(url, tags)
 			if err != nil {
 				log.Print(err)
 			} else {
diff --git a/bot/collection.go b/bot/collection.go
index 38d09d63dfd9957117a9c0d2155ce5371a87230a..49d22cc2326395fea0895f3792596f8e818b9d16 100644
--- a/bot/collection.go
+++ b/bot/collection.go
@@ -2,23 +2,38 @@ package bot
 
 import (
 	"log"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Collection interface {
-	Add(url string) (Content, error)
+	Add(url string, tags []string) (*site.Content, error)
 	Get()
 }
 
 type PlayBotCollection struct {
-	Source string
-	Db     Db
+	Source  string
+	Db      Db
+	Readers []site.Reader
 }
 
-func (pb *PlayBotCollection) Add(url string) (Content, error) {
-	log.Printf("%s: Add() %s", pb.Source, url)
-	content := Content{Title: url}
+func (pb *PlayBotCollection) Add(url string, tags []string) (*site.Content, error) {
+	log.Printf("%s: Add() %s, %q", pb.Source, url, tags)
+
+	var content *site.Content
+	for _, reader := range pb.Readers {
+		var err error
+		content, err = reader.Read(url)
+		if err != nil {
+			return nil, err
+		}
+		if content != nil {
+			break
+		}
+	}
+
 	pb.Db.Create(content)
-	return Content{}, nil
+	return content, nil
 }
 
 func (*PlayBotCollection) Get() {
diff --git a/bot/factory.go b/bot/factory.go
index dbdf0c6d827c319a48b613593c6f8c5779a880bc..9db1936f2b55fe242fe91dc9d64635679ccde847 100644
--- a/bot/factory.go
+++ b/bot/factory.go
@@ -2,28 +2,32 @@ package bot
 
 import (
 	"log"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Factory interface {
 	GetBot(string) Bot
 }
 
-type playBotFactory struct{}
+type playBotFactory struct {
+	readers []site.Reader
+}
 
-func NewPlayBotFactory(dbParams DbParams) *playBotFactory {
-	return &playBotFactory{}
+func NewPlayBotFactory(dbParams DbParams, readers []site.Reader) *playBotFactory {
+	return &playBotFactory{readers}
 }
 
-func (*playBotFactory) GetBot(source string) Bot {
+func (f *playBotFactory) GetBot(source string) Bot {
 	// give DB as arg
 
 	db := &DummyDb{}
-	return NewPlayBot(source, db)
+	return NewPlayBot(source, db, f.readers)
 }
 
 type DummyDb struct{}
 
 func (db *DummyDb) Create(value interface{}) Db {
-	log.Printf("Create(): %s", value)
+	log.Printf("Create(): %q", value)
 	return db
 }
diff --git a/bot/post.go b/bot/post.go
index 096ad4c0b3732d38cf65feb3f40fd7f7c5e0da8c..0a376483d59902ba0e302e5b9c60c05e013aa81e 100644
--- a/bot/post.go
+++ b/bot/post.go
@@ -2,6 +2,8 @@ package bot
 
 import (
 	"time"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Post struct {
@@ -9,7 +11,7 @@ type Post struct {
 	Date    time.Time
 	Author  string `gorm:"column:sender_irc"`
 	Source  string `gorm:"column:chan"`
-	Content Content
+	Content site.Content
 	Tag     Tag
 }
 
diff --git a/main.go b/main.go
index 2cd35abb8bad1b0aebe91fb9b8a541fa3a7def74..a7368b06a80e630d5faccf20055414ce0b4450c7 100644
--- a/main.go
+++ b/main.go
@@ -8,6 +8,7 @@ import (
 	"gopkg.in/yaml.v2"
 
 	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 	"git.iiens.net/morignot2011/playbot/transport/irc"
 )
 
@@ -54,7 +55,16 @@ func main() {
 	var config config
 	err = yaml.Unmarshal(file, &config)
 	quit := make(map[string](chan bool))
-	factory := bot.NewPlayBotFactory(config.Db)
+
+	youtube, err := site.NewYoutube("AIzaSyD32EU9lb9cnNcEaSUBKCkKodk6rjl3HYc")
+	if err != nil {
+		log.Fatal(err)
+	}
+	readers := []site.Reader{
+		//&site.Youtube{},
+		youtube,
+	}
+	factory := bot.NewPlayBotFactory(config.Db, readers)
 
 	for name, config := range config.Transport {
 		c := startTransport(name, config, factory)
diff --git a/bot/content.go b/site/content.go
similarity index 96%
rename from bot/content.go
rename to site/content.go
index 18a44ce7a112b190e2a708bdc67b378b9d1b5067..96109b349cb89845bce56bc0d3273d21f87dc64c 100644
--- a/bot/content.go
+++ b/site/content.go
@@ -1,4 +1,4 @@
-package bot
+package site
 
 type Content struct {
 	Author       string `gorm:"column:sender"`
diff --git a/site/reader.go b/site/reader.go
new file mode 100644
index 0000000000000000000000000000000000000000..f7c42605fcf24ffedaf7f44c6358315f8f60df26
--- /dev/null
+++ b/site/reader.go
@@ -0,0 +1,5 @@
+package site
+
+type Reader interface {
+	Read(url string) (*Content, error)
+}
diff --git a/site/utils.go b/site/utils.go
new file mode 100644
index 0000000000000000000000000000000000000000..aee25b137dbef3e5ee439261f884273bd5785722
--- /dev/null
+++ b/site/utils.go
@@ -0,0 +1,40 @@
+package site
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+)
+
+type NotISO8601 struct {
+	s string
+}
+
+func (e NotISO8601) Error() string {
+	return fmt.Sprintf("'%s' is not in ISO 8601 format")
+}
+
+func ParseISO8601Duration(s string) (int, error) {
+	re := regexp.MustCompile(`^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$`)
+	match := re.FindStringSubmatch(s)
+
+	if len(match) == 0 {
+		return -1, NotISO8601{s}
+	}
+
+	var duration int
+	if match[1] != "" {
+		hours, _ := strconv.Atoi(match[1])
+		duration += 3600 * hours
+	}
+	if match[2] != "" {
+		minutes, _ := strconv.Atoi(match[2])
+		duration += 60 * minutes
+	}
+	if match[3] != "" {
+		seconds, _ := strconv.Atoi(match[3])
+		duration += seconds
+	}
+
+	return duration, nil
+}
diff --git a/site/youtube.go b/site/youtube.go
new file mode 100644
index 0000000000000000000000000000000000000000..c872b471ee28cf4a60dfe1436ed7439786b997bd
--- /dev/null
+++ b/site/youtube.go
@@ -0,0 +1,54 @@
+package site
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"regexp"
+
+	"google.golang.org/api/googleapi/transport"
+	"google.golang.org/api/youtube/v3"
+)
+
+type Youtube struct {
+	service *youtube.Service
+}
+
+func NewYoutube(key string) (Reader, error) {
+	client := &http.Client{
+		Transport: &transport.APIKey{Key: key},
+	}
+
+	service, err := youtube.New(client)
+	return &Youtube{service: service}, err
+}
+
+func (yt Youtube) Read(url string) (*Content, error) {
+	log.Print(url)
+	re := regexp.MustCompile(`(?:^|[^!])https?://(?:www.youtube.com/watch\?[a-zA-Z0-9_=&-]*v=|youtu.be/)([a-zA-Z0-9_-]+)`)
+	match := re.FindStringSubmatch(url)
+	log.Print(match)
+	if len(match) == 0 {
+		return nil, nil
+	}
+
+	response, err := yt.service.Videos.List("snippet,contentDetails").Id(match[1]).Do()
+	if err != nil {
+		return nil, err
+	}
+
+	video := response.Items[0]
+	duration, err := ParseISO8601Duration(video.ContentDetails.Duration)
+	if err != nil {
+		return nil, errors.New(fmt.Sprintf("Invalid duration: %s", err))
+	}
+
+	return &Content{
+		Author:   video.Snippet.ChannelTitle,
+		Duration: duration + 1,
+		Source:   "youtube",
+		SourceId: video.Id,
+		Url:      "https://www.youtube.com/watch?v=" + video.Id,
+	}, nil
+}
diff --git a/tests/bot_test.go b/tests/bot_test.go
index 2c662a7c35805b664770d0e30c070ae2ab040c40..23fadc87b7b0cb588490750da112b02216cec523 100644
--- a/tests/bot_test.go
+++ b/tests/bot_test.go
@@ -1,17 +1,18 @@
 package test
 
 import (
-    "reflect"
+	"reflect"
 	"sort"
 	"testing"
 
 	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 func TestAdd(t *testing.T) {
 	db := &DummyDb{}
 	bot := bot.NewPlayBot("test", db)
-	content, err := bot.Add("http://www.youtube.com/watch?v=IiWapK6WQPg")
+	content, err := bot.Add("http://www.youtube.com/watch?v=IiWapK6WQPg", []string{"hardstle", "raw"})
 
 	if err != nil {
 		t.Fatalf("Receive error %s", err)
@@ -48,43 +49,99 @@ func TestAdd(t *testing.T) {
 	}
 }
 
+func TestExtractTags(t *testing.T) {
+	db := &DummyDb{}
+	bot := bot.NewPlayBot("test", db)
+
+	testCases := []struct {
+		msg  string
+		tags []string
+	}{
+		{
+			msg:  "hello #boy, wa#nna dance?",
+			tags: []string{"boy"},
+		},
+		{
+			msg:  "#I'm #Bond, James #Bond",
+			tags: []string{"I", "Bond"},
+		},
+		{
+			msg:  "last #test but not #least",
+			tags: []string{"test", "least"},
+		},
+	}
+
+	for _, c := range testCases {
+		tags := bot.ExtractTags(c.msg)
+		if !reflect.DeepEqual(tags, c.tags) {
+			t.Error("Expected %q but got %q", c.tags, tags)
+		}
+	}
+}
+
 func TestParseLine(t *testing.T) {
 	testCases := []struct {
 		msg      string
-		expected []string
+		expected []AddArgs
 	}{
 		{
-			msg:      "this is a http://perdu.com test",
-			expected: []string{"http://perdu.com"},
+			msg: "this is a http://perdu.com test",
+			expected: []AddArgs{
+				{
+					url:  "http://perdu.com",
+					tags: nil,
+				},
+			},
 		},
 		{
 			msg:      "nothing here",
-			expected: []string{},
+			expected: nil,
+		},
+		{
+			msg: "some http://giantbatfarts.com/ #with #tags",
+			expected: []AddArgs{
+				{
+					url:  "http://giantbatfarts.com/",
+					tags: []string{"tags", "with"},
+				},
+			},
 		},
 		{
-			msg:      "some http://url.com #with #tags",
-			expected: []string{"http://url.com"},
+			msg: "https://youtu.be/zpG4CZcTpRM?t=1m24s url: http://heeeeeeeey.com/ #oki",
+			expected: []AddArgs{
+				{
+					url:  "http://heeeeeeeey.com/",
+					tags: []string{"oki"},
+				},
+				{
+					url:  "https://youtu.be/zpG4CZcTpRM?t=1m24s",
+					tags: []string{"oki"},
+				},
+			},
 		},
 		{
-			msg:      "http://two.com urls: http://second.com",
-			expected: []string{"http://second.com", "http://two.com"},
+			msg: "fake tag https://translate.google.com/#en/ja/PlayBot",
+			expected: []AddArgs{
+				{
+					url:  "https://translate.google.com/#en/ja/PlayBot",
+					tags: nil,
+				},
+			},
 		},
 	}
 
 	for _, c := range testCases {
-		fc := &FakeCollection{
-            AddCalls: []string{},
-        }
+		fc := &FakeCollection{}
 		testBot := &bot.PlayBot{
 			Collection: fc,
 		}
-		contents := make(chan bot.Content)
+		contents := make(chan site.Content)
 
 		go testBot.ParseLine("me", c.msg, contents)
 		for range contents {
 		}
 
-		sort.Strings(fc.AddCalls)
+		sort.Sort(ByUrl(fc.AddCalls))
 		if !reflect.DeepEqual(fc.AddCalls, c.expected) {
 			t.Errorf("Expected %q but got %q", c.expected, fc.AddCalls)
 		}
diff --git a/tests/dummy.go b/tests/dummy.go
index 527d9fb0cb8f2c5644fffc97d6097c98bf53323e..2bc53d58b2fa726ce6bddaf5d1d17574fb99a5a6 100644
--- a/tests/dummy.go
+++ b/tests/dummy.go
@@ -2,13 +2,14 @@ package test
 
 import (
 	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type DummyDb struct {
-	createCalls []bot.Content
+	createCalls []site.Content
 }
 
 func (db *DummyDb) Create(value interface{}) bot.Db {
-	db.createCalls = append(db.createCalls, value.(bot.Content))
+	db.createCalls = append(db.createCalls, value.(site.Content))
 	return db
 }
diff --git a/tests/mock.go b/tests/mock.go
index 6333ad5c81688f641842fb47c29a7f95bb526803..f044181a2202e0f6aeb41326b480ca345e862004 100644
--- a/tests/mock.go
+++ b/tests/mock.go
@@ -1,17 +1,35 @@
 package test
 
 import (
-	"git.iiens.net/morignot2011/playbot/bot"
+	"sort"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type FakeCollection struct {
-	AddCalls []string
+	AddCalls []AddArgs
+}
+
+type AddArgs struct {
+	url  string
+	tags []string
 }
 
-func (c *FakeCollection) Add(url string) (bot.Content, error) {
-	c.AddCalls = append(c.AddCalls, url)
+type ByUrl []AddArgs
+
+func (a ByUrl) Len() int           { return len(a) }
+func (a ByUrl) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByUrl) Less(i, j int) bool { return a[i].url < a[j].url }
+
+func (c *FakeCollection) Add(url string, tags []string) (site.Content, error) {
+	sort.Strings(tags)
+
+	c.AddCalls = append(c.AddCalls, AddArgs{
+		url:  url,
+		tags: tags,
+	})
 
-	return bot.Content{}, nil
+	return site.Content{}, nil
 }
 
 func (*FakeCollection) Get() {
diff --git a/tests/site_test.go b/tests/site_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..57ea278275a5983097545463c4f6f75e4ef25a08
--- /dev/null
+++ b/tests/site_test.go
@@ -0,0 +1,50 @@
+package test
+
+import (
+	"testing"
+
+	"git.iiens.net/morignot2011/playbot/site"
+)
+
+func TestParseISO8601Duration(t *testing.T) {
+	testCases := []struct {
+		s string
+		i int
+	}{
+		{
+			s: "PT5H",
+			i: 18000,
+		},
+		{
+			s: "PT0S",
+			i: 0,
+		},
+		{
+			s: "P5H3M1S",
+			i: -1,
+		},
+		{
+			s: "PT5H3m1S",
+			i: -1,
+		},
+		{
+			s: "PT5H3M1S",
+			i: 18181,
+		},
+		{
+			s: "PT98M1234S",
+			i: 7114,
+		},
+		{
+			s: "",
+			i: -1,
+		},
+	}
+
+	for _, c := range testCases {
+		d, _ := site.ParseISO8601Duration(c.s)
+		if d != c.i {
+			t.Errorf("Expected %d but got %d for '%s'", c.i, d, c.s)
+		}
+	}
+}
diff --git a/transport/irc/.events.go.swp b/transport/irc/.events.go.swp
new file mode 100644
index 0000000000000000000000000000000000000000..92779672c8ec1df79889d67ee8382586b7a777ee
Binary files /dev/null and b/transport/irc/.events.go.swp differ
diff --git a/transport/irc/.print.go.swp b/transport/irc/.print.go.swp
new file mode 100644
index 0000000000000000000000000000000000000000..853d46928d8b839cbf92af3320f486514dd72286
Binary files /dev/null and b/transport/irc/.print.go.swp differ
diff --git a/transport/irc/events.go b/transport/irc/events.go
index 4292c1d730c1bf8df2fb8f9209717459007e0268..64571102f9508cfa9ea72c574ebd49eb9da1e5cb 100644
--- a/transport/irc/events.go
+++ b/transport/irc/events.go
@@ -1,9 +1,9 @@
 package irc
 
 import (
-	"git.iiens.net/morignot2011/playbot/bot"
-
 	irc "github.com/fluffle/goirc/client"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 func (t *IrcTransport) connected(conn *irc.Conn, line *irc.Line) {
@@ -12,7 +12,7 @@ func (t *IrcTransport) connected(conn *irc.Conn, line *irc.Line) {
 		conn.Join(channel)
 		bot := t.botFactory.GetBot("irc.iiens.net")
 		t.bots[channel] = bot
-		bot.Add("lol.com")
+		bot.Add("lol.com", []string{})
 	}
 }
 
@@ -24,7 +24,7 @@ func (t *IrcTransport) privmsg(conn *irc.Conn, line *irc.Line) {
 	channel := line.Target()
 	msg := line.Args[1]
 	b := t.bots[channel]
-	contents := make(chan bot.Content)
+	contents := make(chan *site.Content)
 
 	go b.ParseLine(line.Nick, msg, contents)
 
diff --git a/transport/irc/print.go b/transport/irc/print.go
index 2c85e53b563406ef3e651e60f44169507a427d4b..e7cc912d916a16916b54160c36ccd727712e8cad 100644
--- a/transport/irc/print.go
+++ b/transport/irc/print.go
@@ -3,11 +3,11 @@ package irc
 import (
 	"log"
 
-	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
-func (t *IrcTransport) printContent(content bot.Content) {
-	log.Printf("Print lol")
+func (t *IrcTransport) printContent(content *site.Content) {
+	log.Printf("Print lol: %q", content)
 }
 
 func (t *IrcTransport) printError(err error) {