НейроАгент

SoundCloud API: получение понравившихся треков в хронологическом порядке

Получение понравившихся треков SoundCloud в точном хронологическом порядке с использованием API v1. Полное руководство с реализацией на Next.js, настройкой OAuth и обработкой пагинации.

Вопрос

Как я могу получить треки, которые понравились вошедшему в систему пользователю, в хронологическом порядке с помощью SoundCloud API v1?

Мне нужно:

  • Получить треки, которые понравились пользователю, в точном порядке, в котором они были отмечены как понравившиеся

Что я уже пробовал:

  • Использование API v2, но я не могу заставить OAuth работать без внутреннего ID
  • Использование эндпоинта истории для получения дат, когда пользователь отмечал треки как понравившиеся, но этот подход не работает
  • Получение понравившихся треков с помощью эндпоинта /me/likes/tracks, но он не включает временную метку created_at

Моя техническая настройка:

  • Next.js 16 (TypeScript)
  • SOUNDCLOUD_CLIENT_ID
  • SOUNDCLOUD_CLIENT_SECRET
НейроАгент

API SoundCloud v1: получение понравившихся треков в хронологическом порядке

API SoundCloud v1 предоставляет ограниченные возможности для получения понравившихся треков в строгом хронологическом порядке, но этого можно достичь, комбинируя несколько конечных точек (endpoints) и правильно обрабатывая постраничную навигацию. Конечная точка /me/likes/tracks не включает временные метки, поэтому вам потребуется использовать конечную точку /me/history для получения временных меток отметок “нравится” и затем сопоставить их с реальными данными треков.

Содержание

Настройка аутентификации в API SoundCloud v1

Для аутентификации с API SoundCloud v1 вам потребуется реализовать поток OAuth 2.0 с учетными данными клиента или код авторизации. Для приложений Next.js обычно более подходит поток кода авторизации, так как он позволяет получить доступ к пользовательским данным.

typescript
// Конфигурация аутентификации SoundCloud
const SOUNDCLOUD_CONFIG = {
  clientId: process.env.SOUNDCLOUD_CLIENT_ID,
  clientSecret: process.env.SOUNDCLOUD_CLIENT_SECRET,
  redirectUri: 'http://localhost:3000/api/soundcloud/callback',
  scope: 'non-expiring' // или необходимые конкретные области доступа
};

Для аутентификации в API SoundCloud v1 вам необходимо:

  1. Инициализировать поток OAuth: Создать URL авторизации, который перенаправит пользователей в SoundCloud для предоставления разрешений
  2. Обрабатывать обратный вызов: Обменять код авторизации на токены доступа
  3. Безопасно хранить токены: Сохранять токены доступа в базе данных или хранилище сессий

Для Next.js можно создать маршруты API для обработки потока OAuth:

typescript
// pages/api/soundcloud/auth.ts
export default function handler(req, res) {
  const authUrl = `https://soundcloud.com/connect?
    client_id=${SOUNDCLOUD_CONFIG.clientId}
    &redirect_uri=${encodeURIComponent(SOUNDCLOUD_CONFIG.redirectUri)}
    &response_type=code
    &scope=${SOUNDCLOUD_CONFIG.scope}`;
  
  res.redirect(authUrl);
}

Получение понравившихся треков с временными метками

Поскольку /me/likes/tracks не включает временные метки, вам потребуется использовать двухэтапный подход:

  1. Получить историю отметок “нравится” с временными метками из /me/history
  2. Получить детали трека для каждого понравившегося трека
typescript
interface LikeHistoryItem {
  created_at: string;
  track: {
    id: number;
    title: string;
    user: { username: string };
    // другие свойства трека
  };
}

async function getLikedTracksWithTimestamps(accessToken: string): Promise<LikeHistoryItem[]> {
  const historyResponse = await fetch('https://api.soundcloud.com/me/history', {
    headers: {
      'Authorization': `OAuth ${accessToken}`
    }
  });
  
  const history = await historyResponse.json();
  return history.filter(item => item.type === 'track' && item.action === 'like');
}

Конечная точка истории SoundCloud возвращает действия пользователя, включая отметки “нравится” с временными метками создания. Каждое событие отметки “нравится” содержит точное время, когда пользователь отметил трек.


Важное замечание: API может возвращать действия в обратном хронологическом порядке, поэтому вам потребуется отсортировать их по created_at в порядке возрастания для получения хронологической последовательности.

Реализация хронологической сортировки в Next.js

Вот полная реализация на Next.js, которая объединяет данные истории и треков:

typescript
// lib/soundcloud.ts
interface SoundCloudConfig {
  clientId: string;
  clientSecret: string;
}

interface LikeHistoryResponse {
  created_at: string;
  type: string;
  action: string;
  track: {
    id: number;
    title: string;
    streamable: boolean;
    user: {
      id: number;
      username: string;
      permalink: string;
    };
    artwork_url?: string;
    duration: number;
    description?: string;
  };
}

interface TrackDetails {
  id: number;
  title: string;
  streamable: boolean;
  user: {
    id: number;
    username: string;
    permalink: string;
  };
  artwork_url?: string;
  duration: number;
  description?: string;
  [key: string]: any; // Дополнительные свойства
}

export class SoundCloudService {
  constructor(private config: SoundCloudConfig) {}

  async getLikedTracksChronologically(accessToken: string): Promise<TrackDetails[]> {
    try {
      // Шаг 1: Получить историю отметок "нравится" с временными метками
      const historyResponse = await fetch('https://api.soundcloud.com/me/history', {
        headers: {
          'Authorization': `OAuth ${accessToken}`
        }
      });

      if (!historyResponse.ok) {
        throw new Error(`Не удалось получить историю: ${historyResponse.statusText}`);
      }

      const history: LikeHistoryResponse[] = await historyResponse.json();
      const likes = history.filter(item => item.type === 'track' && item.action === 'like');

      // Сортировка по created_at в порядке возрастания (сначала старые)
      likes.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());

      // Шаг 2: Получить подробную информацию о каждом треке
      const trackDetails: TrackDetails[] = [];
      
      for (const like of likes) {
        const trackId = like.track.id;
        const trackResponse = await fetch(`https://api.soundcloud.com/tracks/${trackId}`, {
          headers: {
            'Authorization': `OAuth ${accessToken}`
          }
        });

        if (trackResponse.ok) {
          const trackDetails: TrackDetails = await trackResponse.json();
          trackDetails.liked_at = like.created_at; // Добавить временную метку отметки "нравится"
          trackDetails.push(trackDetails);
        }
      }

      return trackDetails;
    } catch (error) {
      console.error('Ошибка при получении понравившихся треков:', error);
      throw error;
    }
  }

  async getAccessToken(authCode: string): Promise<string> {
    const tokenResponse = await fetch('https://soundcloud.com/oauth2/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret,
        redirect_uri: 'http://localhost:3000/api/soundcloud/callback',
        code: authCode
      })
    });

    const tokenData = await tokenResponse.json();
    return tokenData.access_token;
  }
}

Обработка постраничной навигации и ограничений по скорости запросов

API SoundCloud использует постраничную навигацию и имеет ограничения по скорости запросов, которые необходимо правильно обрабатывать:

typescript
interface PaginatedResponse<T> {
  collection: T[];
  next_href?: string;
  prev_href?: string;
}

async function fetchPaginated<T>(
  url: string, 
  accessToken: string, 
  maxPages: number = 10
): Promise<T[]> {
  let allItems: T[] = [];
  let nextUrl = url;
  let pageCount = 0;

  while (nextUrl && pageCount < maxPages) {
    // Добавить задержку для соблюдения ограничений по скорости запросов
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    const response = await fetch(nextUrl, {
      headers: {
        'Authorization': `OAuth ${accessToken}`
      }
    });

    if (!response.ok) {
      throw new Error(`Ошибка запроса к API: ${response.statusText}`);
    }

    const data: PaginatedResponse<T> = await response.json();
    allItems = [...allItems, ...data.collection];
    
    nextUrl = data.next_href;
    pageCount++;
  }

  return allItems;
}

// Использование в основной функции:
async function getLikedTracksChronologically(accessToken: string): Promise<TrackDetails[]> {
  const historyUrl = 'https://api.soundcloud.com/me/history';
  const history = await fetchPaginated<LikeHistoryResponse>(historyUrl, accessToken);
  
  // Фильтрация и сортировка отметок "нравится"
  const likes = history.filter(item => item.type === 'track' && item.action === 'like')
    .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());

  // Получение подробной информации о треках с обработкой постраничной навигации
  const trackDetails = await Promise.all(
    likes.map(async like => {
      const trackResponse = await fetch(`https://api.soundcloud.com/tracks/${like.track.id}`, {
        headers: { 'Authorization': `OAuth ${accessToken}` }
      });
      return trackResponse.json();
    })
  );

  return trackDetails.map((track, index) => ({
    ...track,
    liked_at: likes[index].created_at
  }));
}

Пример полной реализации

Вот полный маршрут API Next.js, который обрабатывает весь процесс:

typescript
// pages/api/soundcloud/liked-tracks.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { SoundCloudService } from '@/lib/soundcloud';

const soundcloudService = new SoundCloudService({
  clientId: process.env.SOUNDCLOUD_CLIENT_ID!,
  clientSecret: process.env.SOUNDCLOUD_CLIENT_SECRET!
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Метод не разрешен' });
  }

  const { accessToken } = req.query;

  if (!accessToken || typeof accessToken !== 'string') {
    return res.status(400).json({ error: 'Токен доступа обязателен' });
  }

  try {
    const likedTracks = await soundcloudService.getLikedTracksChronologically(accessToken);
    res.status(200).json({ likedTracks });
  } catch (error) {
    console.error('Ошибка при получении понравившихся треков:', error);
    res.status(500).json({ 
      error: 'Не удалось получить понравившиеся треки',
      details: error instanceof Error ? error.message : 'Неизвестная ошибка'
    });
  }
}

И клиентский компонент, использующий этот API:

typescript
// components/SoundCloudLikedTracks.tsx
'use client';

import { useState, useEffect } from 'react';

interface Track {
  id: number;
  title: string;
  user: { username: string };
  liked_at: string;
  artwork_url?: string;
  duration: number;
}

export default function SoundCloudLikedTracks() {
  const [tracks, setTracks] = useState<Track[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchLikedTracks = async () => {
      try {
        // Вам потребуется обработать получение токена доступа из вашего потока OAuth
        const accessToken = localStorage.getItem('soundcloud_access_token');
        
        if (!accessToken) {
          setError('Не аутентифицирован с SoundCloud');
          return;
        }

        const response = await fetch('/api/soundcloud/liked-tracks?accessToken=' + accessToken);
        const data = await response.json();

        if (!response.ok) {
          throw new Error(data.error || 'Не удалось получить треки');
        }

        setTracks(data.likedTracks);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Неизвестная ошибка');
      } finally {
        setLoading(false);
      }
    };

    fetchLikedTracks();
  }, []);

  if (loading) return <div>Загрузка ваших понравившихся треков...</div>;
  if (error) return <div>Ошибка: {error}</div>;

  return (
    <div>
      <h2>Ваши понравившиеся треки (в хронологическом порядке)</h2>
      <div className="tracks-list">
        {tracks.map((track) => (
          <div key={track.id} className="track-item">
            {track.artwork_url && (
              <img 
                src={track.artwork_url} 
                alt={track.title}
                className="track-artwork"
              />
            )}
            <div className="track-info">
              <h3>{track.title}</h3>
              <p>от {track.user.username}</p>
              <small>Отмечено "нравится": {new Date(track.liked_at).toLocaleDateString()}</small>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Устранение распространенных проблем

Проблема: Ошибка аутентификации OAuth без внутреннего ID

  • Решение: Убедитесь, что вы используете правильный поток OAuth. Для Next.js используйте поток кода авторизации, а не учетные данные клиента. Поток учетных данных клиента предназначен для аутентификации сервер-сервер и не предоставляет контекст пользователя.

Проблема: Конечная точка истории не возвращает события отметок “нравится”

  • Решение: Убедитесь, что пользователь действительно отметил треки “нравится”, и что область доступа OAuth включает non-expiring или необходимые разрешения. Также проверьте, что вы используете правильную конечную точку API (/me/history).

Проблема: Ограничение по скорости запросов или ошибки API

  • Решение: Реализуйте правильную обработку ошибок и логику повторных попыток. Добавляйте задержки между запросами и корректно обрабатывайте коды HTTP статуса. API SoundCloud имеет ограничения по скорости запросов, которые следует соблюдать.

Проблема: Подробности треков отсутствуют в понравившихся треках

  • Решение: Некоторые треки могут быть приватными или удаленными. Добавьте обработку ошибок для каждого запроса трека и отфильтруйте треки, которые не могут быть получены.

Проблема: Проблемы с постраничной навигацией

  • Решение: Убедитесь, что вы следуете ссылкам в поле next_href ответа API, а не создаете URL вручную. API SoundCloud использует постраничную навигацию на основе курсора.

Профессиональный совет: Рассмотрите возможность кэширования данных понравившихся треков для повышения производительности. Поскольку хронологический порядок не должен часто меняться, вы можете кэшировать результаты на несколько часов и обновлять их только при необходимости.

Источники

  1. Документация API SoundCloud v1
  2. Руководство по аутентификации OAuth 2.0 SoundCloud
  3. Справочник API SoundCloud
  4. Документация маршрутов API Next.js
  5. Рекомендации по ограничению скорости запросов SoundCloud

Заключение

Получение понравившихся треков SoundCloud в строгом хронологическом порядке требует многоэтапного подхода из-за ограничений API. Вот ключевые выводы:

  • Используйте конечную точку /me/history для получения временных меток отметок “нравится”, так как /me/likes/tracks не включает даты создания
  • Комбинируйте два вызова API: сначала получите историю отметок “нравится”, затем получите подробную информацию о треках
  • Реализуйте правильную постраничную навигацию для обработки больших коллекций понравившихся треков
  • Добавьте обработку ошибок для ограничений по скорости запросов, проблем аутентификации и отсутствующих треков
  • Кэшируйте результаты для повышения производительности и уменьшения количества вызовов API

Для вашей настройки Next.js предоставленная реализация должна решить проблемы аутентификации и получения данных, с которыми вы столкнулись. Ключ заключается в работе с ограничениями API, а не в борьбе с ними, путем использования конечной точки истории для получения необходимого хронологического порядка.

Если вам нужна дальнейшая настройка, рассмотрите возможность добавления таких функций, как бесконечная прокрутка, фильтрация треков или механизмы лучшего восстановления ошибок в зависимости от вашего конкретного случая использования.