MP3プレイヤーサンプル(JLayer,BasicPlayer,Java ID3 Tag Library)一通りの機能実装版


サンプルダウンロード

サンプルソース単品 BasicPlayerSample2.java
サンプルソース&必要なjarファイル BasicPlayerSample2.zip
まるっと固めたjarファイル BasicPlayerSample2_jar.zip
Windows用のexe入り BasicPlayerSample2_exe.zip

新しいサンプル

サンプルソース単品 BasicPlayerSample3.java
サンプルソース&必要なjarファイル BasicPlayerSample3.zip
まるっと固めたjarファイル BasicPlayerSample3_jar.zip
Windows用のexe入り BasicPlayerSample3_exe.zip

説明

Java/swing/BasicPlayer/Java ID3 Tag Library/JLayer
BasicPlayerを使ったmp3プレイヤーサンプル第二弾
再生/停止/一時停止/プレイリスト/ループ再生/シャッフル再生/ボリューム調整/シーク/次の曲/前の曲/ID3タグv1v2読み込み
ギリギリ最低限の機能をだいたい載せた気分のmp3プレイヤーサンプルです。BasicPlayerのお陰で結構簡単に作れました。
Java ID3 Tag Libraryは文字化け対応を埋め込んだ改造版です。
プレイリストは起動中に作るのみで保存も、読み込みも出来ません。そのうち実装したい…

サンプルソース(長すぎるけど

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
 
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTable;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.metal.MetalSliderUI;
import javax.swing.table.DefaultTableModel;
 
import javazoom.jlgui.basicplayer.BasicController;
import javazoom.jlgui.basicplayer.BasicPlayer;
import javazoom.jlgui.basicplayer.BasicPlayerEvent;
import javazoom.jlgui.basicplayer.BasicPlayerException;
import javazoom.jlgui.basicplayer.BasicPlayerListener;
 
import org.farng.mp3.MP3File;
import org.farng.mp3.TagException;
import org.farng.mp3.id3.AbstractID3v2;
import org.farng.mp3.id3.ID3v1;
 
// Java/swing/BasicPlayer/Java ID3 Tag Library/JLayer
// BasicPlayerを使ったmp3プレイヤーサンプル第二弾
// 再生/停止/一時停止/プレイリスト/ループ再生/シャッフル再生/ボリューム調整/シーク/次の曲/前の曲/ID3タグv1v2読み込み
// ギリギリ最低限の機能をだいたい載せた気分のmp3プレイヤーサンプルです。BasicPlayerのお陰で結構簡単に作れました。
// Java ID3 Tag Libraryは文字化け対応を埋め込んだ改造版です。
// プレイリストは起動中に作るのみで保存も、読み込みも出来ません。そのうち実装したい…
public class BasicPlayerSample3 extends JFrame {
 
  // ベーシックプレイヤー
  BasicPlayer player;
 
  // Swing用変数、定数
  // ボタン用テキスト 停止
  final static String STOP = "Stop";
  // ボタン用テキスト 再生
  final static String PLAY = "Play";
  // ボタン用テキスト 一時停止
  final static String PAUSE = "Pause";
  // タイトル
  final static String TITLE = "BasicPlayerでmp3再生";
  final static String FILE_NAME = "ファイル名:";
 
  //  プレイリスト再生方法
  String PL_ONE = "1曲のみ";
  String PL_LIST = "プレイリスト順";
  String PL_SHUFFLE = "シャッフル";
 
  // 再生中のインデックス
  int playIndex;
  // プレイリストのモデル
  DefaultTableModel m;
  // 再生ボタン
  JButton bPlay;
  // 曲名ラベル
  JLabel label;
  // 演奏位置スライダー
  JSlider posSlider;
  // ボリュームスライダー
  JSlider volSlider;
  // 演奏位置スライダ用のリスナー
  ChangeListener posSliderCl;
  // ボリュームスライダ用のリスナー
  ChangeListener volSliderCl;
  // ループチェックボックス
  JCheckBox roopCheck;
  // プレイリストJTable
  JTable playList;
  // プレイリストの再生方法コンボボックス
  JComboBox plCombo;
  // シャッフル用のインデックスリスト
  ArrayList<Integer> shIndexs;
  // BasicPlayerからのプロパティを保存する変数
  private Map audioInfo = null;
 
  // メイン関数
  public static void main(String[] args) {
    new BasicPlayerSample3();
  }
 
  // コンストラクタ
  public BasicPlayerSample3() {
    setTitle(TITLE);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setBounds(200, 100, 300, 420);
    setLayout(new FlowLayout());
 
    // ドロップターゲット設定
    new DropTarget(this, DnDConstants.ACTION_COPY, new MyDropTargetListener());
 
    // ラベル設置
    label = new JLabel(" mp3ファイルをドロップしてください。");
    label.setPreferredSize(new Dimension(getWidth() - 10, 20));
    add(label);
    add(getHr(2000, 0));
 
    // 再生、停止ボタン設置
    bPlay = new JButton(PLAY);
    bPlay.addActionListener(new bPlayAction());
    JButton bStop = new JButton(STOP);
    bStop.addActionListener(new bStopAction());
    JButton bNext = new JButton(">>");
    bNext.addActionListener(new bNextAction());
    JButton bPrev = new JButton("<<");
    bPrev.addActionListener(new bPrevAction());
    add(bPrev);
    add(bPlay);
    add(bStop);
    add(bNext);
 
    // 再生位置スライダー設置
    add(getHr(2000, 0));
    add(new JLabel("再生位置"));
    posSlider = new JSlider(0, 100, 0);
    posSliderCl = getPosChangeListener();
    posSlider.addChangeListener(posSliderCl);
    add(posSlider);
 
    // ボリュームスライダー設置
    add(getHr(2000, 0));
    add(new JLabel("ボリューム"));
    volSlider = new JSlider(0, 100, 50);
    volSliderCl = getVolChangeListener();
    volSlider.addChangeListener(volSliderCl);
    volSlider.setUI(new TriSliderUI());
    Dimension d = volSlider.getPreferredSize();
    d.width = 100;
    volSlider.setPreferredSize(d);
    add(volSlider);
 
    add(getHr(2000, 0));
    // プレイリスト再生方法
    plCombo = new JComboBox(new String[] { PL_ONE, PL_LIST, PL_SHUFFLE });
    plCombo.setSelectedIndex(1);
    setPlComboListener();
    add(plCombo);
 
    // ループ再生チェック
    roopCheck = new JCheckBox("ループ再生");
    add(roopCheck);
 
    // プレイリスト
    add(getHr(2000, 0));
    m = new DefaultTableModel(new String[] { "タイトル", "アーティスト" }, 0);
    playList = new JTable(m);
    playList.setDefaultEditor(Object.class, null); // セルの編集禁止
    setPlayListListener();
    JScrollPane sc = new JScrollPane(playList);
    sc.setPreferredSize(new Dimension(280, 200));
    add(sc);
 
    // BasicPlayerのインスタンス作成
    player = new BasicPlayer();
    addBasicPlayerListener();
 
    setVisible(true);
  }
 
  // プレイリストの再生方法コンボボックスのリスナーを設定
  String beforeItem;
 
  private void setPlComboListener() {
    plCombo.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        System.out.println(e.getStateChange());
        if (e.getStateChange() == ItemEvent.SELECTED) {
          if (PL_SHUFFLE.equals(e.getItem())) {
            // シャッフルの場合
            shIndexs = new ArrayList<Integer>();
            for (int i = 0; i < files.size(); i++) {
              shIndexs.add(i);
            }
            Collections.shuffle(shIndexs);
            play(0);
          } else if (PL_SHUFFLE.equals(beforeItem)) {
            // シャッフルから他のアイテムになった場合
            playIndex = shIndexs.get(playIndex);
          }
        } else {
          beforeItem = e.getItem().toString();
        }
      }
    });
  }
 
  // プレイリストにリスナーを設定します
  private void setPlayListListener() {
    playList.addMouseListener(new MouseAdapter() {
      @Override
      public void mouseClicked(MouseEvent e) {
        // リストダブルクリックで該当曲再生
        if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
          int idx = playList.rowAtPoint(e.getPoint());
          if (idx >= 0) {
            play(idx);
          }
        }
        super.mouseClicked(e);
      }
    });
  }
 
  // スライダの変更通知を受け取るリスナーです
  private ChangeListener getPosChangeListener() {
    return new ChangeListener() {
      // スライダを操作すると通知が来ます
      @Override
      public void stateChanged(ChangeEvent e) {
        // マウスで操作中は何もません
        if (!posSlider.getValueIsAdjusting()) {
          // マウスで操作中でなければスライダの位置に合わせてシークします
          try {
            if (player.getStatus() == BasicPlayer.STOPPED) {
              // 停止中の場合、再生開始してからシークします
              play();
            }
            // シーク位置計算
            long l = getAudioBytes() * posSlider.getValue() / 100;
            player.seek(l);
          } catch (BasicPlayerException e1) {
            e1.printStackTrace();
          }
        }
      }
    };
  }
 
  // スライダの変更通知を受け取るリスナーです
  private ChangeListener getVolChangeListener() {
    return new ChangeListener() {
      // スライダを操作すると通知が来ます
      @Override
      public void stateChanged(ChangeEvent e) {
        // マウスで操作中は何もません
        if (!volSlider.getValueIsAdjusting()) {
          // マウスで操作中でなければスライダの位置に合わせてシークします
          setGain();
        }
      }
    };
  }
 
  // スライダに合わせてボリュームを設定します
  private void setGain() {
    try {
      player.setGain((double) volSlider.getValue() / volSlider.getMaximum());
    } catch (BasicPlayerException e1) {
      e1.printStackTrace();
    }
  }
 
  // BasicPlayerリスナーを追加します
  BasicPlayerListener bpl;
 
  private void addBasicPlayerListener() {
    bpl = new BasicPlayerListener() {
      // プレイヤーのステータス変更通知
      @Override
      public void stateUpdated(BasicPlayerEvent event) {
        if (event.getCode() == BasicPlayerEvent.EOM) {
          try {
            stopAction();
          } catch (BasicPlayerException e) {
            e.printStackTrace();
          }
        }
        // プレイヤーのステータスが停止になった場合
        if (player.getStatus() == BasicPlayer.STOPPED) {
        }
      }
 
      @Override
      public void setController(BasicController controller) {
      }
 
      // 再生中の進捗通知
      @Override
      public void progress(int bytesread, long microseconds, byte[] pcmdata, Map properties) {
        // 再生が進んだらスライダを操作します。
        if (getAudioBytes() > 0) {
          // 新しいスライダの位置を計算します
          int newValue = (int) (bytesread * 100 / getAudioBytes());
          // スライダをマウスで摘んでる間は操作しません
          // スライダの値と、現在の値が同じ場合も操作しません
          if (!posSlider.getValueIsAdjusting() && posSlider.getValue() != newValue) {
            // setValueでの操作でも、ChangeListenerに通知が行くのでリスナー取り出し、操作、リスナー追加の順に処理します
            posSlider.removeChangeListener(posSliderCl);
            posSlider.setValue(newValue);
            posSlider.addChangeListener(posSliderCl);
          }
        }
      }
 
      // ファイルオープン通知
      @Override
      public void opened(Object stream, Map properties) {
        // プロパティをクラス変数に保存します
        audioInfo = properties;
      }
    };
    player.addBasicPlayerListener(bpl);
  }
 
  // 曲の最後まできた時のアクション
  private void stopAction() throws BasicPlayerException {
    if (plCombo.getSelectedIndex() == 0) {
      // 1曲のみ
      if (roopCheck.isSelected()) {
        // ループならまた再生
//        stop();
        play();
      } else {
        // ループでなければ停止
        stop();
      }
    } else if (plCombo.getSelectedIndex() == 1) {
      // プレイリスト順
      if (roopCheck.isSelected()) {
        // ループなら次のファイルへ
        next();
      } else {
        if (playIndex + 1 >= files.size()) {
          // ループじゃない&最後まで来てたら停止
          stop();
        } else {
          // 最後じゃなければ次のファイルへ
          next();
        }
      }
    } else {
      // SHUFFLE
      if (roopCheck.isSelected()) {
        // ループなら次のファイルへ
        next();
      } else {
        if (playIndex + 1 >= files.size()) {
          // ループじゃない&最後まで来てたら停止
          stop();
        } else {
          // 最後じゃなければ次のファイルへ
          next();
        }
      }
    }
  }
 
  // プロパティから音楽ファイルの長さを返します
  private long getAudioBytes() {
    try {
      return Long.parseLong(audioInfo.get("audio.length.bytes").toString());
    } catch (Exception e) {
      return 0;
    }
  }
 
  // 水平線
  public JSeparator getHr(int width, int hight) {
    JSeparator sp = new JSeparator(JSeparator.HORIZONTAL);
    sp.setPreferredSize(new Dimension(width, hight));
    return sp;
  }
 
  // 再生ボタンのアクションクラス
  class bPlayAction implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
      try {
        int status = player.getStatus();
        if (status == BasicPlayer.PAUSED) {
          // 一時停止の場合、一時停止を解除します
          resume();
        } else if (status == BasicPlayer.STOPPED) {
          // 停止中の場合、再生開始します
          play();
        } else if (status == BasicPlayer.PLAYING) {
          // 再生中の場合、一時停止します
          pause();
        }
      } catch (Exception ex) {
        // 握りつぶす
      }
    }
  }
 
  // 停止ボタンのアクションクラス
  class bStopAction implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
      try {
        stop();
      } catch (Exception ex) {
        // 握りつぶす
      }
    }
  }
 
  // 次ボタンのアクションクラス
  class bNextAction implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
      try {
        next();
      } catch (Exception ex) {
        // 握りつぶす
      }
    }
  }
 
  private void next() {
    System.out.println("next");
    play(playIndex + 1);
  }
 
  private void prev() {
    System.out.println("prev");
    play(playIndex - 1);
  }
 
  // 前ボタンのアクションクラス
  class bPrevAction implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
      try {
        prev();
      } catch (Exception ex) {
        // 握りつぶす
      }
    }
  }
 
  // 一時停止
  private void pause() throws BasicPlayerException {
    // playMode = PLAY_MODE_PAUSE;
    player.pause();
    bPlay.setText(PLAY);
  }
 
  // 一時停止解除
  private void resume() throws BasicPlayerException {
    player.resume();
    bPlay.setText(PAUSE);
  }
 
  // 再生
  private void play() throws BasicPlayerException {
    if (player.getStatus() != BasicPlayer.STOPPED) {
      player.stop();
    }
    player.play();
    bPlay.setText(PAUSE);
  }
 
  // インデックス指定で再生
  private void play(int idx) {
    try {
      if (files.size() == 0) {
        return;
      }
      if (files.size() <= idx) {
        playIndex = 0;
      } else if (0 > idx) {
        playIndex = files.size() - 1;
      } else {
        playIndex = idx;
      }
      if (plCombo.getSelectedIndex() == 2) {
        // シャッフル
        open(files.get(shIndexs.get(playIndex)));
      } else {
        // 通常
        open(files.get(playIndex));
      }
    } catch (BasicPlayerException e) {
      e.printStackTrace();
    }
    if (plCombo.getSelectedIndex() == 2) {
      // シャッフル
      playList.setRowSelectionInterval(shIndexs.get(playIndex), shIndexs.get(playIndex));
    } else {
      // 通常
      playList.setRowSelectionInterval(playIndex, playIndex);
    }
  }
 
  // 停止
  private void stop() throws BasicPlayerException {
    if (player.getStatus() != BasicPlayer.STOPPED) {
      player.stop();
    }
    bPlay.setText(PLAY);
  }
 
  // 開く
  private void open(File file) throws BasicPlayerException {
    if (player.getStatus() == BasicPlayer.PLAYING) {
      // 一旦停止してから次のファイルを再生&リスナーがいると次のファイルを再生しようとするので一旦取り出し。
      player.removeBasicPlayerListener(bpl);
      player.stop();
      for (int i = 0; i < 100; i++) {
        if (player.getStatus() == BasicPlayer.STOPPED) {
          break;
        }
        i++;
        try {
          Thread.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      player.addBasicPlayerListener(bpl);
    }
    // ラベル設定
    label.setText(FILE_NAME + file.getName());
    // 取得したファイルを開く
    player.open(file);
    play(); // 再生
  }
 
  private void setPlayList() {
    int rowCount = m.getRowCount();
    for (int i = rowCount; i < files.size(); i++) {
      File f = files.get(i);
      m.addRow(readTag(f));
    }
  }
 
  // mp3ファイルからID3タグを読んで表示します
  private String[] readTag(File file) {
    MP3File mp3file;
    String[] ret = new String[2];
    try {
      mp3file = new MP3File(file);
 
      String title = file.getName();
      String artist = "--";
      // v2タグを優先的に表示
      if (mp3file.hasID3v2Tag()) {
        AbstractID3v2 v2 = mp3file.getID3v2Tag();
        title = v2.getSongTitle();
        artist = v2.getLeadArtist();
 
      } else if (mp3file.hasID3v2Tag()) {
        // v2タグがない場合v1タグを表示
        ID3v1 v1 = mp3file.getID3v1Tag();
 
        byte[] ary = v1.getTitle().getBytes("ISO-8859-1");
        title = new String(ary);
        ary = v1.getArtist().getBytes("ISO-8859-1");
        artist = new String(ary);
      }
 
      // 読み込んだタグを画面に設定
      if (title == null || title.trim().length() == 0) {
        title = file.getName();
      }
      ret[0] = title;
      ret[1] = artist;
 
    } catch (IOException e) {
      e.printStackTrace();
    } catch (TagException e) {
      e.printStackTrace();
    }
 
    return ret;
  }
 
  List<File> files = new ArrayList<File>();
 
  // ドロップターゲットリスナー
  // ドロップされたファイルを受け取り、最初のファイルだけを再生します
  class MyDropTargetListener extends DropTargetAdapter {
    @Override
    public void drop(DropTargetDropEvent dtde) {
      dtde.acceptDrop(DnDConstants.ACTION_COPY);
      boolean b = false;
      try {
        if (dtde.getTransferable().isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
          b = true;
          List<File> list = (List<File>) dtde.getTransferable().getTransferData(
              DataFlavor.javaFileListFlavor);
 
          int idx = files.size();
          // 最初のファイルだけ取得
          for (File file : list) {
            if (file.getName().toLowerCase().endsWith("mp3")) {
              files.add(file);
            }
          }
          setPlayList();
          play(idx);
        }
      } catch (Exception e) {
        e.printStackTrace();
      } finally {
        dtde.dropComplete(b);
      }
    }
  }
}
 
// スライダー描画クラス
class TriSliderUI extends MetalSliderUI {
 
  Color trackBackGroundColor = Color.LIGHT_GRAY;
  Color trackColor = Color.WHITE;
  Color trackBorderColor = Color.BLACK;
  Color thumbColor = Color.LIGHT_GRAY;
 
  public TriSliderUI() {
    super();
  }
 
  public TriSliderUI(Color trackBackGroundColor, Color trackColor, Color trackBorderColor,
      Color thumbColor) {
    super();
    this.trackBackGroundColor = trackBackGroundColor;
    this.trackColor = trackColor;
    this.trackBorderColor = trackBorderColor;
    this.thumbColor = thumbColor;
  }
 
  public Color getTrackBackGroundColor() {
    return trackBackGroundColor;
  }
 
  public void setTrackBackGroundColor(Color trackBackGroundColor) {
    this.trackBackGroundColor = trackBackGroundColor;
  }
 
  public Color getTrackColor() {
    return trackColor;
  }
 
  public void setTrackColor(Color trackColor) {
    this.trackColor = trackColor;
  }
 
  public Color getTrackBorderColor() {
    return trackBorderColor;
  }
 
  public void setTrackBorderColor(Color trackBorderColor) {
    this.trackBorderColor = trackBorderColor;
  }
 
  public Color getThumbColor() {
    return thumbColor;
  }
 
  public void setThumbColor(Color thumbColor) {
    this.thumbColor = thumbColor;
  }
 
  // マーカー描画スレッド
  @Override
  public void paintThumb(Graphics g) {
    if (slider.getOrientation() == JSlider.HORIZONTAL) {
      Graphics2D g2 = (Graphics2D) g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setPaint(thumbColor);
      int x = thumbRect.x + thumbRect.width / 2 - 2;
      int y = thumbRect.y;
      int w = 5;
      int h = thumbRect.height;
      g2.fill3DRect(x, y, w, h, true);
    } else {
      super.paintThumb(g);
    }
  }
 
  // トラック描画スレッド
  @Override
  public void paintTrack(Graphics g) {
    int cx, cy, cw, ch;
    int pad;
    int pady = 3;
    Rectangle trackBounds = trackRect;
    if (slider.getOrientation() == JSlider.HORIZONTAL) {
      Graphics2D g2 = (Graphics2D) g;
      // いろいろ座標設定
      pad = trackBuffer;
      cx = pad;
      cy = trackBounds.height / 2 - pady;
      cw = trackBounds.width;
 
      // アンチエイリアス設定
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.translate(trackBounds.x, trackBounds.y + cy + pady);
 
      // トラックの背景描画(ライトグレー
      g2.setPaint(trackBackGroundColor);
      Polygon polygon1 = new Polygon();
      polygon1.addPoint(0, cy);
      polygon1.addPoint(cw, cy);
      polygon1.addPoint(cw, -cy);
      g2.fillPolygon(polygon1);
 
      // トラックの支点から現在地までの描画(白
      g2.setPaint(trackColor);
      Polygon polygon2 = new Polygon();
      polygon2.addPoint(0, cy);
      polygon2.addPoint(thumbRect.x, cy);
      polygon2.addPoint(thumbRect.x, cy - (int) (cy * 2 / (double) cw * thumbRect.x));
      g2.fillPolygon(polygon2);
      polygon2.reset();
 
      // 黒い枠線描画
      g2.setPaint(trackBorderColor);
      g2.drawPolygon(polygon1);
 
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
      g2.translate(-trackBounds.x, -(trackBounds.y + cy + pady));
    } else {
      super.paintTrack(g);
    }
  }
}
 
 

コメント

  • バグ報告です、1回のみリピートの時一旦停止しないと再生し直せないみたいです。要はplay();の一行前にstop();を入れればいいだけの話なんですが。 - Daxy 2013-02-12 01:30:03
    • バグ報告ありがとうございますっ今週は忙しいので来週調べてみます>< - ちゃぱてぃ 2013-02-14 02:26:47
      • えいやえいやと修正してみました。ついでにボリュームのスライダーを別のサンプルで作ったのに差し替え。BasicPlayerSample3になりました。 - ちゃぱてぃ 2013-02-22 02:15:13
最終更新:2013年02月22日 01:54