diff --git a/server/lib/data_import/my_anime_list.rb b/server/lib/data_import/my_anime_list.rb
new file mode 100644
index 000000000..f1fca7274
--- /dev/null
+++ b/server/lib/data_import/my_anime_list.rb
@@ -0,0 +1,45 @@
+module DataImport
+ class MyAnimeList
+ ATARASHII_API_HOST = 'https://hbv3-mal-api.herokuapp.com/2.1/'.freeze
+
+ include DataImport::Media
+ include DataImport::HTTP
+
+ attr_reader :opts
+
+ def initialize(opts = {})
+ @opts = opts.with_indifferent_access
+ super()
+ end
+
+ def get_media(external_id) # anime/1234 or manga/1234
+ media = Mapping.lookup('myanimelist', external_id)
+ # should return Anime or Manga
+ klass = external_id.split('/').first.classify.constantize
+ # initialize the class
+ media ||= klass.new
+
+ get(external_id) do |response|
+ details = Extractor::Media.new(response)
+
+ media.assign_attributes(details.to_h.compact)
+ media.genres = details.genres.map { |genre|
+ Genre.find_by(name: genre)
+ }.compact
+
+ yield media
+ end
+ end
+
+ private
+
+ def get(url, opts = {})
+ super(build_url(url), opts)
+ end
+
+ def build_url(path)
+ return path if path.include?('://')
+ "#{ATARASHII_API_HOST}#{path}"
+ end
+ end
+end
diff --git a/server/lib/data_import/my_anime_list/extractor/media.rb b/server/lib/data_import/my_anime_list/extractor/media.rb
new file mode 100644
index 000000000..f71c9a11e
--- /dev/null
+++ b/server/lib/data_import/my_anime_list/extractor/media.rb
@@ -0,0 +1,160 @@
+module DataImport
+ class MyAnimeList
+ module Extractor
+ class Media
+ attr_reader :data
+
+ def initialize(json)
+ @data = JSON.parse(json)
+ end
+
+ def age_rating
+ return unless data['classification']
+ rating = data['classification'].split(' - ')
+
+ case rating[0]
+ when 'G', 'TV-Y7' then :G
+ when 'PG', 'PG13' then :PG
+ when 'R', 'R+' then :R
+ when 'Rx' then :R18
+ end
+ end
+
+ def episode_count
+ data['episodes']
+ end
+
+ def episode_length
+ data['duration']
+ end
+
+ def synopsis
+ clean_desc(data['synopsis'])
+ end
+
+ def youtube_video_id
+ data['preview']&.split('/')&.last
+ end
+
+ def poster_image
+ data['image_url']
+ end
+
+ def age_rating_guide
+ return unless data['classification']
+ rating = data['classification'].split(' - ')
+
+ return 'Violence, Profanity' if rating[0] == 'R'
+ return rating[1] if rating[1].present?
+
+ # fallback
+ case rating[0]
+ when 'G' then 'All Ages'
+ when 'PG' then 'Children'
+ when 'PG13', 'PG-13' then 'Teens 13 or older'
+ # when 'R' then 'Violence, Profanity'
+ # this will NEVER happen because of return
+ when 'R+' then 'Mild Nudity'
+ when 'Rx' then 'Hentai'
+ end
+ end
+
+ def subtype # will be renamed to this hopefully
+ # anime matches [TV special OVA ONA movie music]
+ # manga matches [manga novel manhua oneshot doujin]
+ type = data['type'].downcase
+
+ case type
+ when 'tv' then :TV
+ when 'ova' then :OVA
+ when 'ona' then :ONA
+ else type.to_sym
+ end
+ end
+
+ def start_date
+ data['start_date']&.to_date
+ end
+
+ def end_date
+ data['end_date']&.to_date
+ end
+
+ def titles
+ {
+ en_jp: data['title'],
+ en_us: data['other_titles']['english'].try(:first),
+ ja_jp: data['other_titles']['japanese'].try(:first)
+ }
+ end
+
+ def abbreviated_titles
+ data['other_titles']['synonyms']
+ end
+
+ # Manga Specific
+
+ def chapters
+ data['chapters']
+ end
+
+ def volumes
+ data['volumes']
+ end
+
+ # TODO: removed subtype (show_type, manga_type issue)
+ # TODO: missing status on manga (anime does automagically)
+ def to_h
+ %i[age_rating episode_count episode_length synopsis youtube_video_id
+ poster_image age_rating_guide start_date end_date
+ titles abbreviated_titles chapters volumes]
+ .map { |k|
+ [k, send(k)]
+ }.to_h
+ end
+
+ def genres
+ data['genres']
+ end
+
+ # synopsis: seriously don't touch this unless you are Nuck.
+ def br_to_p(src)
+ src = '
' + src.gsub(/ \s* /, '
') + '
'
+ doc = Nokogiri::HTML.fragment src
+ doc.traverse do |x|
+ next x.remove if x.name == 'br' && x.previous.nil?
+ next x.remove if x.name == 'br' && x.next.nil?
+ next x.remove if x.name == 'br' && x.next.name == 'p' && x.previous.name == 'p'
+ next x.remove if x.name == 'p' && x.content.blank?
+ end
+ doc.inner_html.gsub(/[\r\n\t]/, '')
+ end
+
+ # synopsis: seriously don't touch this unless you are Nuck.
+ def clean_desc(desc)
+ desc = Nokogiri::HTML.fragment br_to_p(desc)
+ desc.css('.spoiler').each do |x|
+ x.name = 'span'
+ x.inner_html = x.css('.spoiler_content').inner_html
+ x.css('input').remove
+ end
+ desc.css('.spoiler').wrap('
')
+ desc.xpath('descendant::comment()').remove
+ desc.css('b').each { |b| b.replace(b.content) }
+ desc.traverse do |node|
+ next unless node.text?
+ t = node.content.split(/: ?/).map { |x| x.split(' ') }
+ if t.length >= 2
+ if t[0].length <= 3 && t[1].length <= 20
+ node.remove
+ end
+ else
+ node.remove if /^\s+\*\s+.*/ =~ node.content
+ end
+ end
+ desc.inner_html
+ end
+ end
+ end
+ end
+end
diff --git a/server/spec/fixtures/image.jpg b/server/spec/fixtures/image.jpg
new file mode 100644
index 000000000..1914264c0
Binary files /dev/null and b/server/spec/fixtures/image.jpg differ
diff --git a/server/spec/fixtures/my_anime_list/berserk-manga.json b/server/spec/fixtures/my_anime_list/berserk-manga.json
new file mode 100644
index 000000000..494ea4572
--- /dev/null
+++ b/server/spec/fixtures/my_anime_list/berserk-manga.json
@@ -0,0 +1,79 @@
+{
+ "id": 2,
+ "title": "Berserk",
+ "other_titles": {
+ "english": [
+ "Berserk"
+ ],
+ "synonyms": [
+ "Berserk: The Prototype"
+ ],
+ "japanese": [
+ "\u30d9\u30eb\u30bb\u30eb\u30af"
+ ]
+ },
+ "rank": 1,
+ "popularity_rank": 8,
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/manga\/1\/157931.jpg",
+ "type": "Manga",
+ "status": "publishing",
+ "members_score": 9.24,
+ "members_count": 104891,
+ "favorited_count": 24537,
+ "synopsis": "Guts, a former mercenary now known as the \"Black Swordsman,\" is out for revenge.",
+ "genres": [
+ "Action",
+ "Adventure",
+ "Demons",
+ "Drama",
+ "Fantasy",
+ "Horror",
+ "Supernatural",
+ "Military",
+ "Psychological",
+ "Seinen"
+ ],
+ "tags": [
+
+ ],
+ "anime_adaptations": [
+ {
+ "anime_id": 33,
+ "title": "Berserk",
+ "url": "http:\/\/myanimelist.net\/anime\/33\/Berserk"
+ },
+ {
+ "anime_id": 10218,
+ "title": "Berserk: Ougon Jidai-hen I - Haou no Tamago",
+ "url": "http:\/\/myanimelist.net\/anime\/10218\/Berserk__Ougon_Jidai-hen_I_-_Haou_no_Tamago"
+ },
+ {
+ "anime_id": 12113,
+ "title": "Berserk: Ougon Jidai-hen II - Doldrey Kouryaku",
+ "url": "http:\/\/myanimelist.net\/anime\/12113\/Berserk__Ougon_Jidai-hen_II_-_Doldrey_Kouryaku"
+ },
+ {
+ "anime_id": 12115,
+ "title": "Berserk: Ougon Jidai-hen III - Kourin",
+ "url": "http:\/\/myanimelist.net\/anime\/12115\/Berserk__Ougon_Jidai-hen_III_-_Kourin"
+ },
+ {
+ "anime_id": 32379,
+ "title": "Berserk (2016)",
+ "url": "http:\/\/myanimelist.net\/anime\/32379\/Berserk_2016"
+ }
+ ],
+ "related_manga": [
+ {
+ "manga_id": 92299,
+ "title": "Berserk: Shinen no Kami 2",
+ "url": "http:\/\/myanimelist.net\/manga\/92299\/Berserk__Shinen_no_Kami_2"
+ }
+ ],
+ "alternative_versions": [
+
+ ],
+ "personal_tags": [
+
+ ]
+}
diff --git a/server/spec/fixtures/my_anime_list/cowboy-bebop-movie.json b/server/spec/fixtures/my_anime_list/cowboy-bebop-movie.json
new file mode 100644
index 000000000..9ec18c86a
--- /dev/null
+++ b/server/spec/fixtures/my_anime_list/cowboy-bebop-movie.json
@@ -0,0 +1,562 @@
+{
+ "id": 5,
+ "title": "Cowboy Bebop: Tengoku no Tobira",
+ "preview": "http:\/\/www.youtube.com\/embed\/hc7IxJ93jtM",
+ "other_titles": {
+ "english": [
+ "Cowboy Bebop: The Movie"
+ ],
+ "synonyms": [
+ "Cowboy Bebop: Knockin' on Heaven's Door"
+ ],
+ "japanese": [
+ "\u30ab\u30a6\u30dc\u30fc\u30a4\u30d3\u30d0\u30c3\u30d7\u5929\u56fd\u306e\u6249"
+ ]
+ },
+ "rank": 148,
+ "popularity_rank": 356,
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/6\/14331.jpg",
+ "type": "Movie",
+ "episodes": 1,
+ "status": "finished airing",
+ "start_date": "2001-09-01",
+ "duration": 54,
+ "classification": "R - 17+ (violence & profanity)",
+ "members_score": 8.41,
+ "members_count": 126475,
+ "favorited_count": 518,
+ "external_links": [
+
+ ],
+ "synopsis": "As the Cowboy Bebop crew travels the stars, they learn of the largest bounty yet, a huge 300 million Woolongs. Apparently, someone is wielding a hugely powerful chemical weapon, and of course the authorities are at a loss to stop it. The war to take down the most dangerous criminal yet forces the crew to face a true madman, with bare hope to succeed. \r\n(Source: ANN) ",
+ "background": "No background information has been added to this title.",
+ "producers": [
+ "Sunrise",
+ "Bandai Visual"
+ ],
+ "genres": [
+ "Action",
+ "Drama",
+ "Mystery",
+ "Sci-Fi",
+ "Space"
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "parent_story": {
+ "anime_id": 1,
+ "title": "Cowboy Bebop",
+ "url": "http:\/\/myanimelist.net\/anime\/1\/Cowboy_Bebop"
+ },
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+ "\"Ask DNA\" by The Seatbelts featuring Raju Ramayya"
+ ],
+ "ending_theme": [
+ "\"Gotta Knock a Little Harder\" by The Seatbelts featuring Mai Yamane"
+ ],
+ "recommendations": [
+ {
+ "id": 4106,
+ "title": "Trigun: Badlands Rumble",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/11\/27969.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 913,
+ "title": "Fullmetal Alchemist: The Sacred Star of Milos",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/2\/29550.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 1796,
+ "title": "Dirty Pair: The Movie",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/12\/46805.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 1226,
+ "title": "Seihou Tenshi Angel Links",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/4\/46233.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 1811,
+ "title": "Bounty Dog: Getsumen no Ibu",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/10\/27945.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 21339,
+ "title": "Psycho-Pass Movie",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/8\/71793.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 22,
+ "title": "Metropolis",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/4\/75601.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 122,
+ "title": "Full Moon wo Sagashite",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/9\/6265.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ }
+ ],
+ "personal_tags": [
+
+ ]
+}
diff --git a/server/spec/fixtures/my_anime_list/cowboy-bebop-tv.json b/server/spec/fixtures/my_anime_list/cowboy-bebop-tv.json
new file mode 100644
index 000000000..316f4ca91
--- /dev/null
+++ b/server/spec/fixtures/my_anime_list/cowboy-bebop-tv.json
@@ -0,0 +1,1526 @@
+{
+ "id": 1,
+ "title": "Cowboy Bebop",
+ "preview": "http:\/\/www.youtube.com\/embed\/qig4KOK2R2g",
+ "other_titles": {
+ "english": [
+ "Cowboy Bebop"
+ ],
+ "synonyms": [
+ "COWBOY BEBOP"
+ ],
+ "japanese": [
+ "\u30ab\u30a6\u30dc\u30fc\u30a4\u30d3\u30d0\u30c3\u30d7"
+ ]
+ },
+ "rank": 21,
+ "popularity_rank": 32,
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/4\/19644.jpg",
+ "type": "TV",
+ "episodes": 26,
+ "status": "finished airing",
+ "start_date": "1998-04-03",
+ "end_date": "1999-04-24",
+ "duration": 24,
+ "classification": "R - 17+ (violence & profanity)",
+ "members_score": 8.83,
+ "members_count": 431480,
+ "favorited_count": 27004,
+ "external_links": [
+
+ ],
+ "synopsis": "In fake data with some spoiler I am a spoiler!
\r\n[Written by MAL Rewrite]",
+ "background": "When Cowboy Bebop<\/i> first aired in spring of 1998 on TV Tokyo, only 12 of the show's 26 episodes aired. Anime censorship had increased following the big controversies over Evangelion<\/i>, and most of Bebop<\/i> was pulled from the air due to violent content. Satelite channel WOWOW picked up the show in the fall of that year and aired the full series uncensored. It wasn't a ratings hit in Japan, but nevertheless was well-liked enough to win Best Male Character (for Spike Spiegel) and Best Voice Actor (for Megumi Hayashibara as Faye Valentine) at the 1999 and 2000 Anime Grand Prix. Its biggest influence has been in America, where it premiered on Adult Swim in 2001 with many reruns since. The show's heavy Western influence struck a chord with American viewers, where it became a \"gateway drug\" to anime aimed at adult audiences.",
+ "producers": [
+ "Bandai Visual"
+ ],
+ "genres": [
+ "Action",
+ "Adventure",
+ "Comedy",
+ "Drama",
+ "Sci-Fi",
+ "Space"
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+ {
+ "manga_id": 173,
+ "title": "Cowboy Bebop",
+ "url": "http:\/\/myanimelist.net\/manga\/173\/Cowboy_Bebop"
+ },
+ {
+ "manga_id": 174,
+ "title": "Shooting Star Bebop: Cowboy Bebop",
+ "url": "http:\/\/myanimelist.net\/manga\/174\/Shooting_Star_Bebop__Cowboy_Bebop"
+ }
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+ {
+ "anime_id": 5,
+ "title": "Cowboy Bebop: Tengoku no Tobira",
+ "url": "http:\/\/myanimelist.net\/anime\/5\/Cowboy_Bebop__Tengoku_no_Tobira"
+ },
+ {
+ "anime_id": 17205,
+ "title": "Cowboy Bebop: Ein no Natsuyasumi",
+ "url": "http:\/\/myanimelist.net\/anime\/17205\/Cowboy_Bebop__Ein_no_Natsuyasumi"
+ }
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+ {
+ "anime_id": 4037,
+ "title": "Cowboy Bebop: Yose Atsume Blues",
+ "url": "http:\/\/myanimelist.net\/anime\/4037\/Cowboy_Bebop__Yose_Atsume_Blues"
+ }
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+ "\"Tank!\" by The Seatbelts (eps 1-25)"
+ ],
+ "ending_theme": [
+ "#1: \"The Real Folk Blues\" by The Seatbelts feat. Mai Yamane (eps 1-12, 14-25)",
+ "#2: \"Space Lion\" by The Seatbelts (ep 13)",
+ "#3: \"Blue\" by The Seatbelts feat. Mai Yamane (ep 26)"
+ ],
+ "recommendations": [
+ {
+ "id": 205,
+ "title": "Samurai Champloo",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/11\/29134.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 889,
+ "title": "Black Lagoon",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/4\/25596.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 6,
+ "title": "Trigun",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/7\/20310.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 400,
+ "title": "Seihou Bukyou Outlaw Star",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/7\/28848.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 225,
+ "title": "Baccano!",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/3\/14547.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 4087,
+ "title": "Michiko to Hatchin",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/10\/59535.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 2025,
+ "title": "Darker than Black: Kuro no Keiyakusha",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/5\/19570.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 20057,
+ "title": "Space\u2606Dandy",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/4\/56611.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 360,
+ "title": "Psycho-Pass",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/5\/43399.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 98,
+ "title": "Gintama",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/2\/10038.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 24439,
+ "title": "Kekkai Sensen",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/12\/73559.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 467,
+ "title": "Ghost in the Shell: Stand Alone Complex",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/11\/50857.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 42,
+ "title": "Lupin III",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/10\/15625.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 790,
+ "title": "Ergo Proxy",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/11\/6259.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 202,
+ "title": "Wolf's Rain",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/5\/59403.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 329,
+ "title": "Planetes",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/8\/50463.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 94,
+ "title": "Coyote Ragtime Show",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/5\/21158.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 68,
+ "title": "Black Cat",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/7\/4197.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 474,
+ "title": "Macross Plus",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/6\/22779.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 6746,
+ "title": "Durarara!!",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/10\/71772.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 4,
+ "title": "Gun x Sword",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/3\/25380.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 245,
+ "title": "Great Teacher Onizuka",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/13\/11460.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 567,
+ "title": "The Big O",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/1\/567.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ },
+ {
+ "id": 267,
+ "title": "Gungrave",
+ "other_titles": {
+
+ },
+ "image_url": "http:\/\/cdn.myanimelist.net\/images\/anime\/11\/75724.jpg",
+ "external_links": [
+
+ ],
+ "producers": [
+
+ ],
+ "genres": [
+
+ ],
+ "tags": [
+
+ ],
+ "manga_adaptations": [
+
+ ],
+ "prequels": [
+
+ ],
+ "sequels": [
+
+ ],
+ "side_stories": [
+
+ ],
+ "character_anime": [
+
+ ],
+ "spin_offs": [
+
+ ],
+ "summaries": [
+
+ ],
+ "alternative_versions": [
+
+ ],
+ "other": [
+
+ ],
+ "opening_theme": [
+
+ ],
+ "ending_theme": [
+
+ ],
+ "recommendations": [
+
+ ],
+ "personal_tags": [
+
+ ]
+ }
+ ],
+ "personal_tags": [
+
+ ]
+}
diff --git a/server/spec/lib/data_import/my_anime_list/extractor/media_spec.rb b/server/spec/lib/data_import/my_anime_list/extractor/media_spec.rb
new file mode 100644
index 000000000..c475e473d
--- /dev/null
+++ b/server/spec/lib/data_import/my_anime_list/extractor/media_spec.rb
@@ -0,0 +1,426 @@
+require 'rails_helper'
+
+RSpec.describe DataImport::MyAnimeList::Extractor::Media do
+ let(:tv) { fixture('my_anime_list/cowboy-bebop-tv.json') }
+ let(:movie) { fixture('my_anime_list/cowboy-bebop-movie.json') }
+ let(:manga) { fixture('my_anime_list/berserk-manga.json') }
+
+ subject { described_class.new(tv) }
+ context 'Anime' do
+ describe '#age_rating' do
+ it 'should be a symbol' do
+ expect(subject.age_rating).to be_a(Symbol)
+ end
+
+ it 'should extract the G rating' do
+ subject = described_class.new({
+ classification: 'G - All Ages'
+ }.to_json)
+ expect(subject.age_rating).to eq(:G)
+ end
+
+ it 'should convert TV-Y7 rating to G' do
+ subject = described_class.new({
+ classification: 'TV-Y7 - All Ages'
+ }.to_json)
+ expect(subject.age_rating).to eq(:G)
+ end
+
+ it 'should extract the PG rating' do
+ subject = described_class.new({
+ classification: 'PG - Children'
+ }.to_json)
+ expect(subject.age_rating).to eq(:PG)
+ end
+
+ it 'should convert the PG13 rating to PG' do
+ subject = described_class.new({
+ classification: 'PG13 - Teens 13 or older'
+ }.to_json)
+ expect(subject.age_rating).to eq(:PG)
+ end
+
+ # this exists in fixture
+ it 'should extract the R rating' do
+ expect(subject.age_rating).to eq(:R)
+ end
+
+ it 'should convert the R+ rating to R' do
+ subject = described_class.new({
+ classification: 'R+ - Mild Nudity'
+ }.to_json)
+ expect(subject.age_rating).to eq(:R)
+ end
+
+ it 'should extract the R18 rating' do
+ subject = described_class.new({
+ classification: 'Rx - Hentai'
+ }.to_json)
+ expect(subject.age_rating).to eq(:R18)
+ end
+ end
+
+ describe '#episode_count' do
+ context 'for a tv show' do
+ it 'should return the number of episodes' do
+ expect(subject.episode_count).to eq(26)
+ end
+ end
+
+ context 'for a movie' do
+ subject { described_class.new(movie) }
+
+ it 'should return 1' do
+ expect(subject.episode_count).to eq(1)
+ end
+ end
+ end
+
+ describe '#episode_length' do
+ it 'should return the runtime in minutes' do
+ expect(subject.episode_length).to eq(24)
+ end
+ end
+
+ describe '#synopsis' do
+ it 'should return the plaintext synopsis' do
+ amount = subject.synopsis.scan(/ /).length
+ expect(amount).to eq(3)
+ end
+ end
+
+ describe '#youtube_video_id' do
+ it 'should return the end of the youtube link' do
+ expect(subject.youtube_video_id).to eq('qig4KOK2R2g')
+ end
+ end
+
+ describe '#poster_image' do
+ it 'should return an image link' do
+ expect(subject.poster_image).to eq('http://cdn.myanimelist.net/images/anime/4/19644.jpg')
+ end
+ end
+
+ describe '#age_rating_guide' do
+ it 'should extract the G rating description' do
+ subject = described_class.new({
+ classification: 'G - All Ages'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('All Ages')
+ end
+
+ it 'should extract the PG rating description' do
+ subject = described_class.new({
+ classification: 'PG - Children'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('Children')
+ end
+
+ # check both cases
+ it 'should extract the PG13 & PG-13 rating description' do
+ subject = described_class.new({
+ classification: 'PG13 - Teens 13 or older'
+ }.to_json)
+ subject1 = described_class.new({
+ classification: 'PG-13 - Teens 13 or older'
+ }.to_json)
+
+ expect(subject.age_rating_guide).to eq('Teens 13 or older')
+ expect(subject1.age_rating_guide).to eq('Teens 13 or older')
+ end
+
+ # this exists in fixture
+ it 'should extract the R rating description' do
+ expect(subject.age_rating_guide).to eq('Violence, Profanity')
+ end
+
+ it 'should extract the R+ rating description' do
+ subject = described_class.new({
+ classification: 'R+ - Mild Nudity'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('Mild Nudity')
+ end
+
+ it 'should extract the Rx rating description' do
+ subject = described_class.new({
+ classification: 'Rx - Hentai'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('Hentai')
+ end
+
+ it 'case statement for G' do
+ subject = described_class.new({
+ classification: 'G'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('All Ages')
+ end
+
+ it 'case statement for PG' do
+ subject = described_class.new({
+ classification: 'PG'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('Children')
+ end
+
+ it 'case statement for PG13 & PG-13' do
+ subject = described_class.new({
+ classification: 'PG13'
+ }.to_json)
+ subject1 = described_class.new({
+ classification: 'PG-13'
+ }.to_json)
+
+ expect(subject.age_rating_guide).to eq('Teens 13 or older')
+ expect(subject1.age_rating_guide).to eq('Teens 13 or older')
+ end
+
+ it 'case statement for R+' do
+ subject = described_class.new({
+ classification: 'R+'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('Mild Nudity')
+ end
+
+ it 'case statement for Rx' do
+ subject = described_class.new({
+ classification: 'Rx'
+ }.to_json)
+ expect(subject.age_rating_guide).to eq('Hentai')
+ end
+ end
+
+ describe '#subtype' do
+ it 'should return a Symbol' do
+ expect(subject.subtype).to be_a(Symbol)
+ end
+
+ it 'should return tv show' do
+ expect(subject.subtype).to eq(:TV)
+ end
+
+ it 'should return special' do
+ subject = described_class.new({
+ type: 'Special'
+ }.to_json)
+ expect(subject.subtype).to eq(:special)
+ end
+
+ it 'should return OVA' do
+ subject = described_class.new({
+ type: 'OVA'
+ }.to_json)
+ expect(subject.subtype).to eq(:OVA)
+ end
+
+ it 'should return ONA' do
+ subject = described_class.new({
+ type: 'ONA'
+ }.to_json)
+ expect(subject.subtype).to eq(:ONA)
+ end
+
+ it 'should return movie' do
+ subject = described_class.new(movie)
+ expect(subject.subtype).to eq(:movie)
+ end
+
+ it 'should return music' do
+ subject = described_class.new({
+ type: 'Music'
+ }.to_json)
+ expect(subject.subtype).to eq(:music)
+ end
+ end
+
+ describe '#start_date' do
+ it 'should return a Date object' do
+ expect(subject.start_date).to be_a(Date)
+ end
+ it 'should return the date the first episode aired' do
+ # this is a really stupid test imo.
+ expect(subject.start_date).to eq('1998-04-03'.to_date)
+ end
+ end
+
+ describe '#end_date' do
+ context 'for a tv series' do
+ it 'should return the date the series ended' do
+ expect(subject.end_date).to eq('1999-04-24'.to_date)
+ end
+ end
+
+ context 'for a movie' do
+ subject { described_class.new(movie) }
+
+ it 'should return nil' do
+ expect(subject.end_date).to be_nil
+ end
+ end
+ end
+
+ describe '#titles' do
+ it 'should return the Romaji title of media' do
+ expect(subject.titles[:en_jp]).to eq('Cowboy Bebop')
+ end
+ it 'should return the English title of media' do
+ expect(subject.titles[:en_us]).to eq('Cowboy Bebop')
+ end
+ it 'should return the Japanese title of media' do
+ expect(subject.titles[:ja_jp]).to eq('カウボーイビバップ')
+ end
+ it 'should return nil if English or Japanese title does not exist' do
+ subject = described_class.new({ other_titles: {} }.to_json)
+
+ expect(subject.titles[:en_us]).to be_nil
+ expect(subject.titles[:ja_jp]).to be_nil
+ end
+ end
+
+ describe '#abbreviated_titles' do
+ it 'should return an array' do
+ expect(subject.abbreviated_titles).to be_an(Array)
+ end
+ it 'should return all synonyms titles' do
+ expect(subject.abbreviated_titles).to eq(['COWBOY BEBOP'])
+ end
+ end
+
+ describe '#genres' do
+ it 'should return an array' do
+ expect(subject.genres).to be_an(Array)
+ end
+
+ it 'should return all genres' do
+ expect(subject.genres).to eq(%w[Action Adventure Comedy
+ Drama Sci-Fi Space])
+ end
+ end
+
+ describe '#to_h' do
+ it 'should return a Hash' do
+ expect(subject.to_h).to be_a(Hash)
+ end
+ end
+ end
+
+ context 'Manga' do
+ subject { described_class.new(manga) }
+
+ describe '#synopsis' do
+ it 'should return the plaintext synopsis' do
+ amount = subject.synopsis.scan(/
/).length
+ expect(amount).to eq(1)
+ end
+ end
+
+ describe '#poster_image' do
+ it 'should return an image link' do
+ expect(subject.poster_image).to eq('http://cdn.myanimelist.net/images/manga/1/157931.jpg')
+ end
+ end
+
+ describe '#subtype' do
+ it 'should return a Symbol' do
+ expect(subject.subtype).to be_a(Symbol)
+ end
+
+ it 'should return manga' do
+ expect(subject.subtype).to eq(:manga)
+ end
+
+ it 'should return novel' do
+ subject = described_class.new({
+ type: 'Novel'
+ }.to_json)
+ expect(subject.subtype).to eq(:novel)
+ end
+
+ it 'should return manuha' do
+ subject = described_class.new({
+ type: 'Manuha'
+ }.to_json)
+ expect(subject.subtype).to eq(:manuha)
+ end
+
+ it 'should return oneshot' do
+ subject = described_class.new({
+ type: 'oneshot'
+ }.to_json)
+ expect(subject.subtype).to eq(:oneshot)
+ end
+
+ it 'should return doujin' do
+ subject = described_class.new({
+ type: 'Doujin'
+ }.to_json)
+ expect(subject.subtype).to eq(:doujin)
+ end
+ end
+
+ describe '#titles' do
+ it 'should return the Romaji title of media' do
+ expect(subject.titles[:en_jp]).to eq('Berserk')
+ end
+ it 'should return the English title of media' do
+ expect(subject.titles[:en_us]).to eq('Berserk')
+ end
+ it 'should return the Japanese title of media' do
+ expect(subject.titles[:ja_jp]).to eq('ベルセルク')
+ end
+ it 'should return nil if English or Japanese title does not exist' do
+ subject = described_class.new({ other_titles: {} }.to_json)
+
+ expect(subject.titles[:en_us]).to be_nil
+ expect(subject.titles[:ja_jp]).to be_nil
+ end
+ end
+
+ describe '#abbreviated_titles' do
+ it 'should return an array' do
+ expect(subject.abbreviated_titles).to be_an(Array)
+ end
+ it 'should return all synonyms titles' do
+ expect(subject.abbreviated_titles).to eq(['Berserk: The Prototype'])
+ end
+ end
+
+ describe '#genres' do
+ it 'should return an array' do
+ expect(subject.genres).to be_an(Array)
+ end
+
+ it 'should return all genres' do
+ expect(subject.genres).to eq(%w[Action Adventure Demons
+ Drama Fantasy Horror
+ Supernatural Military
+ Psychological Seinen])
+ end
+ end
+
+ describe '#chapters' do
+ it 'should return total chapters' do
+ subject = described_class.new({
+ chapters: 100
+ }.to_json)
+ expect(subject.chapters).to eq(100)
+ end
+
+ it 'should return nil if no chapters' do
+ expect(subject.chapters).to be_nil
+ end
+ end
+
+ describe '#volumes' do
+ it 'should return total volumes' do
+ subject = described_class.new({
+ volumes: 15
+ }.to_json)
+ expect(subject.volumes).to eq(15)
+ end
+
+ it 'should return nil if no volumes' do
+ expect(subject.volumes).to be_nil
+ end
+ end
+ end
+end
diff --git a/server/spec/lib/data_import/my_anime_list_spec.rb b/server/spec/lib/data_import/my_anime_list_spec.rb
new file mode 100644
index 000000000..0b62e0a05
--- /dev/null
+++ b/server/spec/lib/data_import/my_anime_list_spec.rb
@@ -0,0 +1,69 @@
+require 'rails_helper'
+
+RSpec.describe DataImport::MyAnimeList do
+ subject { described_class.new }
+
+ before do
+ host = described_class::ATARASHII_API_HOST
+ stub_request(:get, "#{host}anime/1")
+ .to_return(body: fixture('my_anime_list/cowboy-bebop-tv.json'))
+ stub_request(:get, "#{host}anime/5")
+ .to_return(body: fixture('my_anime_list/cowboy-bebop-movie.json'))
+ stub_request(:get, 'http://cdn.myanimelist.net/images/anime/4/19644.jpg')
+ .to_return(body: fixture('image.jpg'), headers: {
+ 'Content-Type': 'image/jpg'
+ })
+
+ stub_request(:get, "#{host}manga/1")
+ .to_return(body: fixture('my_anime_list/berserk-manga.json'))
+ stub_request(:get, 'http://cdn.myanimelist.net/images/manga/1/157931.jpg')
+ .to_return(body: fixture('image.jpg'), headers: {
+ 'Content-Type': 'image/jpg'
+ })
+ end
+ context 'Anime' do
+ describe '#get_media' do
+ it 'should yield a Media object' do
+ expect { |b|
+ subject.get_media('anime/1', &b)
+ subject.run
+ }.to yield_with_args(Media)
+ end
+ it 'should have assigned attributes onto the yielded object' do
+ subject.get_media('anime/1') do |media|
+ expect(media.canonical_title).to eq('Cowboy Bebop')
+ end
+ subject.run
+ end
+ it 'should be valid' do
+ subject.get_media('anime/1') do |media|
+ expect(media).to be_valid
+ end
+ subject.run
+ end
+ end
+ end
+
+ context 'Manga' do
+ describe '#get_media' do
+ it 'should yield a Media object' do
+ expect { |b|
+ subject.get_media('manga/1', &b)
+ subject.run
+ }.to yield_with_args(Media)
+ end
+ it 'should have assigned attributes onto the yielded object' do
+ subject.get_media('manga/1') do |media|
+ expect(media.canonical_title).to eq('Berserk')
+ end
+ subject.run
+ end
+ it 'should be valid' do
+ subject.get_media('manga/1') do |media|
+ expect(media).to be_valid
+ end
+ subject.run
+ end
+ end
+ end
+end