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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! The purpose of this library is to provide high-level access to the MyAnimeList API.
//! It allows you to search for anime / manga on MyAnimeList, as well as add / update / delete anime from a user's list.
//! 
//! # Examples
//! 
//! ```no_run
//! use mal::{MAL, SeriesInfo};
//! use mal::list::{AnimeList, ListEntry, Status};
//! 
//! // Create a new MAL instance
//! let mal = MAL::new("username", "password");
//! 
//! // Search for "Toradora" on MyAnimeList
//! let mut search_results = mal.search("Toradora").unwrap();
//! 
//! // Use the first result's info
//! let toradora_info = search_results.swap_remove(0);
//! 
//! // Create a new anime list entry with Toradora's info
//! let mut entry = ListEntry::new(toradora_info);
//! 
//! // Set the entry's watched episodes to 5 and status to watching
//! entry.set_watched_episodes(5).set_status(Status::Watching);
//! 
//! // Add the entry to the user's anime list
//! mal.anime_list().add(&entry).unwrap();
//! ```

#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;

pub mod list;

mod request;
mod util;

extern crate chrono;
extern crate minidom;
extern crate reqwest;

use chrono::NaiveDate;
use failure::{Error, ResultExt, SyncFailure};
use list::AnimeList;
use minidom::Element;
use request::RequestURL;
use reqwest::StatusCode;
use std::convert::Into;

/// Represents basic information of an anime series on MyAnimeList.
#[derive(Debug, Clone)]
pub struct SeriesInfo {
    /// The ID of the anime series.
    pub id: u32,
    /// The title of the anime series.
    pub title: String,
    /// The number of episodes in the anime series.
    pub episodes: u32,
    /// The date the series started airing.
    pub start_date: Option<NaiveDate>,
    /// The date the series finished airing.
    pub end_date: Option<NaiveDate>,
}

impl PartialEq for SeriesInfo {
    #[inline]
    fn eq(&self, other: &SeriesInfo) -> bool {
        self.id == other.id
    }
}

/// Used to interact with the MyAnimeList API with authorization being handled automatically.
#[derive(Debug)]
pub struct MAL {
    /// The user's name on MyAnimeList.
    pub username: String,
    /// The user's password on MyAnimeList.
    pub password: String,
    /// The client used to send requests to the API.
    pub client: reqwest::Client,
}

impl MAL {
    /// Creates a new instance of the MAL struct for interacting with the MyAnimeList API.
    ///
    /// If you only need to retrieve the user's anime list, then you do not need to provide a valid password.
    #[inline]
    pub fn new<S: Into<String>>(username: S, password: S) -> MAL {
        MAL::with_client(username, password, reqwest::Client::new())
    }

    /// Creates a new instance of the MAL struct for interacting with the MyAnimeList API.
    ///
    /// If you only need to retrieve the user's anime list, then you do not need to provide a valid password.
    #[inline]
    pub fn with_client<S: Into<String>>(username: S, password: S, client: reqwest::Client) -> MAL {
        MAL {
            username: username.into(),
            password: password.into(),
            client,
        }
    }

    /// Searches MyAnimeList for an anime and returns all found results.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use mal::MAL;
    ///
    /// let mal = MAL::new("username", "password");
    /// let found = mal.search("Cowboy Bebop").unwrap();
    ///
    /// assert!(found.len() > 0);
    /// ```
    pub fn search(&self, name: &str) -> Result<Vec<SeriesInfo>, Error> {
        let mut resp = request::auth_get(self, RequestURL::Search(name))?;

        if resp.status() == StatusCode::NoContent {
            return Ok(Vec::new());
        }

        let root: Element = resp.text()?.parse().map_err(SyncFailure::new)?;

        let mut entries = Vec::new();

        for child in root.children() {
            let get_child = |name| {
                util::get_xml_child_text(child, name)
                    .context("failed to parse MAL response")
            };

            let entry = SeriesInfo {
                id: get_child("id")?.parse()?,
                title: get_child("title")?,
                episodes: get_child("episodes")?.parse()?,
                start_date: util::parse_str_date(&get_child("start_date")?),
                end_date: util::parse_str_date(&get_child("end_date")?),
            };

            entries.push(entry);
        }

        Ok(entries)
    }

    /// Returns true if the provided account credentials are correct.
    /// 
    /// # Examples
    /// 
    /// ```no_run
    /// use mal::MAL;
    /// 
    /// // Create a new MAL instance
    /// let mal = MAL::new("username", "password");
    /// 
    /// // Verify that the username and password are valid
    /// let valid = mal.verify_credentials().unwrap();
    /// 
    /// assert_eq!(valid, false);
    /// ```
    #[inline]
    pub fn verify_credentials(&self) -> Result<bool, Error> {
        let resp = request::auth_get(self, RequestURL::VerifyCredentials)?;
        Ok(resp.status() == StatusCode::Ok)
    }

    /// Returns a new [AnimeList] instance to allow operations on the user's list.
    /// 
    /// [AnimeList]: ./list/struct.AnimeList.html
    #[inline]
    pub fn anime_list(&self) -> AnimeList {
        AnimeList::new(self)
    }
}